File indexing completed on 2025-03-09 04:54:13

0001 /*
0002  * SPDX-License-Identifier: LGPL-2.1-or-later
0003  *
0004  */
0005 
0006 #include "mailinglist.h"
0007 
0008 #include "messagecore_debug.h"
0009 #include <KConfig>
0010 #include <KConfigGroup>
0011 #include <QUrl>
0012 
0013 #include <QSharedData>
0014 #include <QStringList>
0015 
0016 using namespace MessageCore;
0017 
0018 using MagicDetectorFunc = QString (*)(const KMime::Message::Ptr &, QByteArray &, QString &);
0019 
0020 /* Sender: (owner-([^@]+)|([^@+]-owner)@ */
0021 static QString check_sender(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
0022 {
0023     QString header = message->sender()->asUnicodeString();
0024 
0025     if (header.isEmpty()) {
0026         return {};
0027     }
0028 
0029     if (header.left(6) == QLatin1StringView("owner-")) {
0030         headerName = "Sender";
0031         headerValue = header;
0032         header = header.mid(6, header.indexOf(QLatin1Char('@')) - 6);
0033     } else {
0034         const int index = header.indexOf(QLatin1StringView("-owner@ "));
0035         if (index == -1) {
0036             return {};
0037         }
0038 
0039         header.truncate(index);
0040         headerName = "Sender";
0041         headerValue = header;
0042     }
0043 
0044     return header;
0045 }
0046 
0047 /* X-BeenThere: ([^@]+) */
0048 static QString check_x_beenthere(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
0049 {
0050     QString header;
0051     if (auto hrd = message->headerByType("X-BeenThere")) {
0052         header = hrd->asUnicodeString();
0053     }
0054     if (header.isNull() || header.indexOf(QLatin1Char('@')) == -1) {
0055         return {};
0056     }
0057 
0058     headerName = "X-BeenThere";
0059     headerValue = header;
0060     header.truncate(header.indexOf(QLatin1Char('@')));
0061 
0062     return header;
0063 }
0064 
0065 /* Delivered-To:: <([^@]+) */
0066 static QString check_delivered_to(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
0067 {
0068     QString header;
0069     if (auto hrd = message->headerByType("Delivered-To")) {
0070         header = hrd->asUnicodeString();
0071     }
0072     if (header.isNull() || header.left(13) != QLatin1StringView("mailing list") || header.indexOf(QLatin1Char('@')) == -1) {
0073         return {};
0074     }
0075 
0076     headerName = "Delivered-To";
0077     headerValue = header;
0078 
0079     return header.mid(13, header.indexOf(QLatin1Char('@')) - 13);
0080 }
0081 
0082 /* X-Mailing-List: <?([^@]+) */
0083 static QString check_x_mailing_list(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
0084 {
0085     QString header;
0086     if (auto hrd = message->headerByType("X-Mailing-List")) {
0087         header = hrd->asUnicodeString();
0088     }
0089     if (header.isEmpty()) {
0090         return {};
0091     }
0092 
0093     if (header.indexOf(QLatin1Char('@')) < 1) {
0094         return {};
0095     }
0096 
0097     headerName = "X-Mailing-List";
0098     headerValue = header;
0099     if (header[0] == QLatin1Char('<')) {
0100         header = header.mid(1, header.indexOf(QLatin1Char('@')) - 1);
0101     } else {
0102         header.truncate(header.indexOf(QLatin1Char('@')));
0103     }
0104 
0105     return header;
0106 }
0107 
0108 /* List-Id: [^<]* <([^.]+) */
0109 static QString check_list_id(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
0110 {
0111     QString header;
0112     if (auto hrd = message->headerByType("List-Id")) {
0113         header = hrd->asUnicodeString();
0114     }
0115     if (header.isEmpty()) {
0116         return {};
0117     }
0118 
0119     const int leftAnglePos = header.indexOf(QLatin1Char('<'));
0120     if (leftAnglePos < 0) {
0121         return {};
0122     }
0123 
0124     const int firstDotPos = header.indexOf(QLatin1Char('.'), leftAnglePos);
0125     if (firstDotPos < 0) {
0126         return {};
0127     }
0128 
0129     headerName = "List-Id";
0130     headerValue = header.mid(leftAnglePos);
0131     header = header.mid(leftAnglePos + 1, firstDotPos - leftAnglePos - 1);
0132 
0133     return header;
0134 }
0135 
0136 /* List-Post: <mailto:[^< ]*>) */
0137 static QString check_list_post(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
0138 {
0139     QString header;
0140     if (auto hrd = message->headerByType("List-Post")) {
0141         header = hrd->asUnicodeString();
0142     }
0143     if (header.isEmpty()) {
0144         return {};
0145     }
0146 
0147     int leftAnglePos = header.indexOf(QLatin1StringView("<mailto:"));
0148     if (leftAnglePos < 0) {
0149         return {};
0150     }
0151 
0152     headerName = "List-Post";
0153     headerValue = header;
0154     header = header.mid(leftAnglePos + 8, header.length());
0155     header.truncate(header.indexOf(QLatin1Char('@')));
0156 
0157     return header;
0158 }
0159 
0160 /* Mailing-List: list ([^@]+) */
0161 static QString check_mailing_list(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
0162 {
0163     QString header;
0164     if (auto hrd = message->headerByType("Mailing-List")) {
0165         header = hrd->asUnicodeString();
0166     }
0167     if (header.isEmpty()) {
0168         return {};
0169     }
0170 
0171     if (header.left(5) != QLatin1StringView("list ") || header.indexOf(QLatin1Char('@')) < 5) {
0172         return {};
0173     }
0174 
0175     headerName = "Mailing-List";
0176     headerValue = header;
0177     header = header.mid(5, header.indexOf(QLatin1Char('@')) - 5);
0178 
0179     return header;
0180 }
0181 
0182 /* X-Loop: ([^@]+) */
0183 static QString check_x_loop(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
0184 {
0185     QString header;
0186     if (auto hrd = message->headerByType("X-Loop")) {
0187         header = hrd->asUnicodeString();
0188     }
0189     if (header.isEmpty()) {
0190         return {};
0191     }
0192 
0193     const int indexOfHeader(header.indexOf(QLatin1Char('@')));
0194     if (indexOfHeader < 2) {
0195         return {};
0196     }
0197 
0198     headerName = "X-Loop";
0199     headerValue = header;
0200     header.truncate(indexOfHeader);
0201 
0202     return header;
0203 }
0204 
0205 /* X-ML-Name: (.+) */
0206 static QString check_x_ml_name(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
0207 {
0208     QString header;
0209     if (auto hrd = message->headerByType("X-ML-Name")) {
0210         header = hrd->asUnicodeString();
0211     }
0212     if (header.isEmpty()) {
0213         return {};
0214     }
0215 
0216     headerName = "X-ML-Name";
0217     headerValue = header;
0218     header.truncate(header.indexOf(QLatin1Char('@')));
0219 
0220     return header;
0221 }
0222 
0223 static const MagicDetectorFunc magic_detectors[] = {check_list_id,
0224                                                     check_list_post,
0225                                                     check_sender,
0226                                                     check_x_mailing_list,
0227                                                     check_mailing_list,
0228                                                     check_delivered_to,
0229                                                     check_x_beenthere,
0230                                                     check_x_loop,
0231                                                     check_x_ml_name};
0232 
0233 static QStringList headerToAddress(const QString &header)
0234 {
0235     QStringList addresses;
0236     if (header.isEmpty()) {
0237         return addresses;
0238     }
0239 
0240     int start = 0;
0241     while ((start = header.indexOf(QLatin1Char('<'), start)) != -1) {
0242         int end = 0;
0243         if ((end = header.indexOf(QLatin1Char('>'), ++start)) == -1) {
0244             qCWarning(MESSAGECORE_LOG) << "Serious mailing list header parsing error!";
0245             return addresses;
0246         }
0247 
0248         addresses.append(header.mid(start, end - start));
0249     }
0250 
0251     return addresses;
0252 }
0253 
0254 class Q_DECL_HIDDEN MessageCore::MailingList::MailingListPrivate : public QSharedData
0255 {
0256 public:
0257     MailingListPrivate()
0258         : mFeatures(None)
0259         , mHandler(KMail)
0260     {
0261     }
0262 
0263     MailingListPrivate(const MailingListPrivate &other)
0264         : QSharedData(other)
0265     {
0266         mFeatures = other.mFeatures;
0267         mHandler = other.mHandler;
0268         mPostUrls = other.mPostUrls;
0269         mSubscribeUrls = other.mSubscribeUrls;
0270         mUnsubscribeUrls = other.mUnsubscribeUrls;
0271         mHelpUrls = other.mHelpUrls;
0272         mArchiveUrls = other.mArchiveUrls;
0273         mOwnerUrls = other.mOwnerUrls;
0274         mArchivedAtUrls = other.mArchivedAtUrls;
0275         mId = other.mId;
0276     }
0277 
0278     Features mFeatures;
0279     Handler mHandler;
0280     QList<QUrl> mPostUrls;
0281     QList<QUrl> mSubscribeUrls;
0282     QList<QUrl> mUnsubscribeUrls;
0283     QList<QUrl> mHelpUrls;
0284     QList<QUrl> mArchiveUrls;
0285     QList<QUrl> mOwnerUrls;
0286     QList<QUrl> mArchivedAtUrls;
0287     QString mId;
0288 };
0289 
0290 MailingList MailingList::detect(const KMime::Message::Ptr &message)
0291 {
0292     MailingList mailingList;
0293 
0294     if (auto hrd = message->headerByType("List-Post")) {
0295         mailingList.setPostUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
0296     }
0297 
0298     if (auto hrd = message->headerByType("List-Help")) {
0299         mailingList.setHelpUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
0300     }
0301 
0302     if (auto hrd = message->headerByType("List-Subscribe")) {
0303         mailingList.setSubscribeUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
0304     }
0305 
0306     if (auto hrd = message->headerByType("List-Unsubscribe")) {
0307         mailingList.setUnsubscribeUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
0308     }
0309 
0310     if (auto hrd = message->headerByType("List-Archive")) {
0311         mailingList.setArchiveUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
0312     }
0313 
0314     if (auto hrd = message->headerByType("List-Owner")) {
0315         mailingList.setOwnerUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
0316     }
0317 
0318     if (auto hrd = message->headerByType("Archived-At")) {
0319         mailingList.setArchivedAtUrls(QUrl::fromStringList(headerToAddress(hrd->asUnicodeString())));
0320     }
0321 
0322     if (auto hrd = message->headerByType("List-Id")) {
0323         mailingList.setId(hrd->asUnicodeString());
0324     }
0325 
0326     return mailingList;
0327 }
0328 
0329 QString MailingList::name(const KMime::Message::Ptr &message, QByteArray &headerName, QString &headerValue)
0330 {
0331     QString mailingList;
0332     headerName = QByteArray();
0333     headerValue.clear();
0334 
0335     if (!message) {
0336         return {};
0337     }
0338 
0339     for (const MagicDetectorFunc &detector : magic_detectors) {
0340         mailingList = detector(message, headerName, headerValue);
0341         if (!mailingList.isNull()) {
0342             return mailingList;
0343         }
0344     }
0345 
0346     return {};
0347 }
0348 
0349 MailingList::MailingList()
0350     : d(new MailingListPrivate)
0351 {
0352 }
0353 
0354 MailingList::MailingList(const MailingList &other)
0355 
0356     = default;
0357 
0358 MailingList &MailingList::operator=(const MailingList &other)
0359 {
0360     if (this != &other) {
0361         d = other.d;
0362     }
0363 
0364     return *this;
0365 }
0366 
0367 bool MailingList::operator==(const MailingList &other) const
0368 {
0369     return other.features() == d->mFeatures && other.handler() == d->mHandler && other.postUrls() == d->mPostUrls && other.subscribeUrls() == d->mSubscribeUrls
0370         && other.unsubscribeUrls() == d->mUnsubscribeUrls && other.helpUrls() == d->mHelpUrls && other.archiveUrls() == d->mArchiveUrls
0371         && other.ownerUrls() == d->mOwnerUrls && other.archivedAtUrls() == d->mArchivedAtUrls && other.id() == d->mId;
0372 }
0373 
0374 MailingList::~MailingList() = default;
0375 
0376 MailingList::Features MailingList::features() const
0377 {
0378     return d->mFeatures;
0379 }
0380 
0381 void MailingList::setHandler(MailingList::Handler handler)
0382 {
0383     d->mHandler = handler;
0384 }
0385 
0386 MailingList::Handler MailingList::handler() const
0387 {
0388     return d->mHandler;
0389 }
0390 
0391 void MailingList::setPostUrls(const QList<QUrl> &urls)
0392 {
0393     d->mFeatures |= Post;
0394 
0395     if (urls.empty()) {
0396         d->mFeatures ^= Post;
0397     }
0398 
0399     d->mPostUrls = urls;
0400 }
0401 
0402 QList<QUrl> MailingList::postUrls() const
0403 {
0404     return d->mPostUrls;
0405 }
0406 
0407 void MailingList::setSubscribeUrls(const QList<QUrl> &urls)
0408 {
0409     d->mFeatures |= Subscribe;
0410 
0411     if (urls.empty()) {
0412         d->mFeatures ^= Subscribe;
0413     }
0414 
0415     d->mSubscribeUrls = urls;
0416 }
0417 
0418 QList<QUrl> MailingList::subscribeUrls() const
0419 {
0420     return d->mSubscribeUrls;
0421 }
0422 
0423 void MailingList::setUnsubscribeUrls(const QList<QUrl> &urls)
0424 {
0425     d->mFeatures |= Unsubscribe;
0426 
0427     if (urls.empty()) {
0428         d->mFeatures ^= Unsubscribe;
0429     }
0430 
0431     d->mUnsubscribeUrls = urls;
0432 }
0433 
0434 QList<QUrl> MailingList::unsubscribeUrls() const
0435 {
0436     return d->mUnsubscribeUrls;
0437 }
0438 
0439 void MailingList::setHelpUrls(const QList<QUrl> &urls)
0440 {
0441     d->mFeatures |= Help;
0442 
0443     if (urls.empty()) {
0444         d->mFeatures ^= Help;
0445     }
0446 
0447     d->mHelpUrls = urls;
0448 }
0449 
0450 QList<QUrl> MailingList::helpUrls() const
0451 {
0452     return d->mHelpUrls;
0453 }
0454 
0455 void MailingList::setArchiveUrls(const QList<QUrl> &urls)
0456 {
0457     d->mFeatures |= Archive;
0458 
0459     if (urls.empty()) {
0460         d->mFeatures ^= Archive;
0461     }
0462 
0463     d->mArchiveUrls = urls;
0464 }
0465 
0466 QList<QUrl> MailingList::archiveUrls() const
0467 {
0468     return d->mArchiveUrls;
0469 }
0470 
0471 void MailingList::setOwnerUrls(const QList<QUrl> &urls)
0472 {
0473     d->mFeatures |= Owner;
0474 
0475     if (urls.empty()) {
0476         d->mFeatures ^= Owner;
0477     }
0478 
0479     d->mOwnerUrls = urls;
0480 }
0481 
0482 QList<QUrl> MailingList::ownerUrls() const
0483 {
0484     return d->mOwnerUrls;
0485 }
0486 
0487 void MailingList::setArchivedAtUrls(const QList<QUrl> &urls)
0488 {
0489     d->mFeatures |= ArchivedAt;
0490 
0491     if (urls.isEmpty()) {
0492         d->mFeatures ^= ArchivedAt;
0493     }
0494 
0495     d->mArchivedAtUrls = urls;
0496 }
0497 
0498 QList<QUrl> MailingList::archivedAtUrls() const
0499 {
0500     return d->mArchivedAtUrls;
0501 }
0502 
0503 void MailingList::setId(const QString &id)
0504 {
0505     d->mFeatures |= Id;
0506 
0507     if (id.isEmpty()) {
0508         d->mFeatures ^= Id;
0509     }
0510 
0511     d->mId = id;
0512 }
0513 
0514 QString MailingList::id() const
0515 {
0516     return d->mId;
0517 }
0518 
0519 void MailingList::writeConfig(KConfigGroup &group) const
0520 {
0521     if (d->mFeatures != Feature::None) {
0522         group.writeEntry("MailingListFeatures", static_cast<int>(d->mFeatures));
0523     } else {
0524         group.deleteEntry("MailingListFeatures");
0525     }
0526     if (d->mHandler != Handler::KMail) {
0527         group.writeEntry("MailingListHandler", static_cast<int>(d->mHandler));
0528     } else {
0529         group.deleteEntry("MailingListHandler");
0530     }
0531     if (!d->mId.isEmpty()) {
0532         group.writeEntry("MailingListId", d->mId);
0533     } else {
0534         group.deleteEntry("MailingListId");
0535     }
0536     QStringList lst = QUrl::toStringList(d->mPostUrls);
0537     if (!lst.isEmpty()) {
0538         group.writeEntry("MailingListPostingAddress", lst);
0539     } else {
0540         group.deleteEntry("MailingListPostingAddress");
0541     }
0542 
0543     lst = QUrl::toStringList(d->mSubscribeUrls);
0544     if (!lst.isEmpty()) {
0545         group.writeEntry("MailingListSubscribeAddress", lst);
0546     } else {
0547         group.deleteEntry("MailingListSubscribeAddress");
0548     }
0549 
0550     lst = QUrl::toStringList(d->mUnsubscribeUrls);
0551     if (!lst.isEmpty()) {
0552         group.writeEntry("MailingListUnsubscribeAddress", lst);
0553     } else {
0554         group.deleteEntry("MailingListUnsubscribeAddress");
0555     }
0556 
0557     lst = QUrl::toStringList(d->mArchiveUrls);
0558     if (!lst.isEmpty()) {
0559         group.writeEntry("MailingListArchiveAddress", lst);
0560     } else {
0561         group.deleteEntry("MailingListArchiveAddress");
0562     }
0563 
0564     lst = QUrl::toStringList(d->mOwnerUrls);
0565     if (!lst.isEmpty()) {
0566         group.writeEntry("MailingListOwnerAddress", lst);
0567     } else {
0568         group.deleteEntry("MailingListOwnerAddress");
0569     }
0570 
0571     lst = QUrl::toStringList(d->mHelpUrls);
0572     if (!lst.isEmpty()) {
0573         group.writeEntry("MailingListHelpAddress", lst);
0574     } else {
0575         group.deleteEntry("MailingListHelpAddress");
0576     }
0577 
0578     /* Note: mArchivedAtUrl deliberately not saved here as it refers to a single
0579      * instance of a message rather than an element of a general mailing list.
0580      * http://reviewboard.kde.org/r/1768/#review2783
0581      */
0582 }
0583 
0584 void MailingList::readConfig(const KConfigGroup &group)
0585 {
0586     d->mFeatures = static_cast<MailingList::Features>(group.readEntry("MailingListFeatures", 0));
0587     d->mHandler = static_cast<MailingList::Handler>(group.readEntry("MailingListHandler", static_cast<int>(MailingList::KMail)));
0588     d->mId = group.readEntry("MailingListId");
0589     d->mPostUrls = QUrl::fromStringList(group.readEntry("MailingListPostingAddress", QStringList()));
0590     d->mSubscribeUrls = QUrl::fromStringList(group.readEntry("MailingListSubscribeAddress", QStringList()));
0591     d->mUnsubscribeUrls = QUrl::fromStringList(group.readEntry("MailingListUnsubscribeAddress", QStringList()));
0592     d->mArchiveUrls = QUrl::fromStringList(group.readEntry("MailingListArchiveAddress", QStringList()));
0593     d->mOwnerUrls = QUrl::fromStringList(group.readEntry("MailingListOwnerAddress", QStringList()));
0594     d->mHelpUrls = QUrl::fromStringList(group.readEntry("MailingListHelpAddress", QStringList()));
0595 }