File indexing completed on 2024-11-24 04:52:56

0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0002 
0003    This file is part of the Trojita Qt IMAP e-mail client,
0004    http://trojita.flaska.net/
0005 
0006    This program is free software; you can redistribute it and/or
0007    modify it under the terms of the GNU General Public License as
0008    published by the Free Software Foundation; either version 2 of
0009    the License or (at your option) version 3 or any later version
0010    accepted by the membership of KDE e.V. (or its successor approved
0011    by the membership of KDE e.V.), which shall act as a proxy
0012    defined in Section 14 of version 3 of the license.
0013 
0014    This program is distributed in the hope that it will be useful,
0015    but WITHOUT ANY WARRANTY; without even the implied warranty of
0016    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0017    GNU General Public License for more details.
0018 
0019    You should have received a copy of the GNU General Public License
0020    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0021 */
0022 
0023 #include <algorithm>
0024 #include <QSet>
0025 #include <QStringList>
0026 #include <QUrl>
0027 #include "Recipients.h"
0028 #include "SenderIdentitiesModel.h"
0029 #include "Imap/Model/ItemRoles.h"
0030 #include "Imap/Model/MailboxTree.h"
0031 #include "Imap/Model/Model.h"
0032 
0033 namespace {
0034 
0035 using namespace Composer;
0036 
0037 /** @short Eliminate duplicate identities from the list and preserve just the To, Cc and Bcc fields */
0038 RecipientList deduplicatedAndJustToCcBcc(RecipientList input)
0039 {
0040     QList<Imap::Message::MailAddress> to, cc, bcc;
0041 
0042     Q_FOREACH(const RecipientList::value_type &recipient, input) {
0043         switch (recipient.first) {
0044         case Composer::ADDRESS_TO:
0045             to << recipient.second;
0046             break;
0047         case Composer::ADDRESS_CC:
0048             cc << recipient.second;
0049             break;
0050         case Composer::ADDRESS_BCC:
0051             bcc << recipient.second;
0052             break;
0053         case Composer::ADDRESS_FROM:
0054         case Composer::ADDRESS_SENDER:
0055         case Composer::ADDRESS_REPLY_TO:
0056         case Composer::ADDRESS_RESENT_TO:
0057         case Composer::ADDRESS_RESENT_CC:
0058         case Composer::ADDRESS_RESENT_BCC:
0059         case Composer::ADDRESS_RESENT_SENDER:
0060         case Composer::ADDRESS_RESENT_FROM:
0061             // that's right, ignore these
0062             break;
0063         }
0064     }
0065 
0066     // Keep processing the To, Cc and Bcc fields, making sure that no duplicates (where comparing just the addresses) are present
0067     // in any of them and also making sure that an address is present in at most one of the (To, Cc, Bcc) groups.
0068     RecipientList result;
0069     QSet<QPair<QString, QString> > alreadySeen;
0070 
0071     Q_FOREACH(const Imap::Message::MailAddress &addr, to) {
0072         QPair<QString, QString> item = qMakePair(addr.mailbox, addr.host);
0073         if (!alreadySeen.contains(item)) {
0074             result << qMakePair(Composer::ADDRESS_TO, addr);
0075             alreadySeen.insert(item);
0076         }
0077     }
0078 
0079     Q_FOREACH(const Imap::Message::MailAddress &addr, cc) {
0080         QPair<QString, QString> item = qMakePair(addr.mailbox, addr.host);
0081         if (!alreadySeen.contains(item)) {
0082             result << qMakePair(result.isEmpty() ? Composer::ADDRESS_TO : Composer::ADDRESS_CC, addr);
0083             alreadySeen.insert(item);
0084         }
0085     }
0086 
0087     Q_FOREACH(const Imap::Message::MailAddress &addr, bcc) {
0088         QPair<QString, QString> item = qMakePair(addr.mailbox, addr.host);
0089         if (!alreadySeen.contains(item)) {
0090             result << qMakePair(Composer::ADDRESS_BCC, addr);
0091             alreadySeen.insert(item);
0092         }
0093     }
0094 
0095     return result;
0096 }
0097 
0098 /** @short Mangle the list of recipients according to the stated rules
0099 
0100   The type of each recipient in the input is checked against the mapping. If the mapping has no record for
0101   that type, the recipient is discarded, otherwise the kind is adjusted to the desired value.
0102 
0103 */
0104 RecipientList mapRecipients(RecipientList input, const QMap<RecipientKind, RecipientKind>& mapping)
0105 {
0106     RecipientList::iterator recipient = input.begin();
0107     while (recipient != input.end()) {
0108         QMap<RecipientKind, RecipientKind>::const_iterator operation = mapping.constFind(recipient->first);
0109         if (operation == mapping.constEnd()) {
0110             recipient = input.erase(recipient);
0111         } else if (*operation != recipient->first) {
0112             recipient->first = *operation;
0113             ++recipient;
0114         } else {
0115             // don't modify items which don't need modification
0116             ++recipient;
0117         }
0118     }
0119     return input;
0120 }
0121 
0122 /** @short Replying to all */
0123 bool prepareReplyAll(const RecipientList &originalRecipients, RecipientList &output)
0124 {
0125     QMap<RecipientKind, RecipientKind> mapping;
0126     mapping[ADDRESS_FROM] = ADDRESS_TO;
0127     mapping[ADDRESS_REPLY_TO] = ADDRESS_CC;
0128     mapping[ADDRESS_TO] = ADDRESS_CC;
0129     mapping[ADDRESS_CC] = ADDRESS_CC;
0130     mapping[ADDRESS_BCC] = ADDRESS_BCC;
0131     RecipientList res = deduplicatedAndJustToCcBcc(mapRecipients(originalRecipients, mapping));
0132     if (res.isEmpty()) {
0133         return false;
0134     } else {
0135         output = res;
0136         return true;
0137     }
0138 }
0139 
0140 /** @short Replying to the original author only */
0141 bool prepareReplySenderOnly(const RecipientList &originalRecipients, const QList<QUrl> &headerListPost, RecipientList &output)
0142 {
0143     // Create a blacklist for the Reply-To filtering. This is needed to work with nasty mailing lists (hey, I run quite
0144     // a few like that) which do the reply-to munging.
0145     QList<QPair<QString, QString> > blacklist;
0146     Q_FOREACH(const QUrl &url, headerListPost) {
0147         if (url.scheme().toLower() != QLatin1String("mailto")) {
0148             // non-mail links are not relevant in this situation; they don't mean that we have to give up
0149             continue;
0150         }
0151 
0152         QStringList list = url.path().split(QLatin1Char('@'));
0153         if (list.size() != 2) {
0154             // Malformed mailto: link, maybe it relies on some fancy routing? Either way, play it safe and refuse to work on that
0155             return false;
0156         }
0157 
0158         // FIXME: we actually don't catch the routing!like!this#or#like#this (I don't remember which one is used).
0159         // The routing shall definitely be checked.
0160 
0161         // FIXME: URL decoding? UTF-8 denormalization?
0162         blacklist << qMakePair(list[0].toLower(), list[1].toLower());
0163     }
0164 
0165     // Now gather all addresses from the From and Reply-To headers, taking care to skip munged Reply-To from ML software
0166     RecipientList originalFrom, originalReplyTo;
0167     Q_FOREACH(const RecipientList::value_type &recipient, originalRecipients) {
0168         switch (recipient.first) {
0169         case ADDRESS_FROM:
0170             originalFrom << qMakePair(ADDRESS_TO, recipient.second);
0171             break;
0172         case ADDRESS_REPLY_TO:
0173             if (blacklist.contains(qMakePair(recipient.second.mailbox.toLower(), recipient.second.host.toLower()))) {
0174                 // This is the safe situation, this item in the Reply-To is set to a recognized mailing list address.
0175                 // We can safely ignore that.
0176             } else {
0177                 originalReplyTo << qMakePair(ADDRESS_TO, recipient.second);
0178             }
0179             break;
0180         default:
0181             break;
0182         }
0183     }
0184 
0185     if (!originalReplyTo.isEmpty()) {
0186         // Prefer replying to the (ML-demunged) Reply-To addresses
0187         output = originalReplyTo;
0188         return true;
0189     } else if (!originalFrom.isEmpty()) {
0190         // If no usable thing is in the Reply-To, fall back to anything in From
0191         output = originalFrom;
0192         return true;
0193     } else {
0194         // No recognized addresses -> bail out
0195         return false;
0196     }
0197 }
0198 
0199 /** @short Helper: replying to the list */
0200 bool prepareReplyList(const QList<QUrl> &headerListPost, const bool headerListPostNo, RecipientList &output)
0201 {
0202     if (headerListPostNo)
0203         return false;
0204 
0205     RecipientList res;
0206     Q_FOREACH(const QUrl &url, headerListPost) {
0207         if (url.scheme().toLower() != QLatin1String("mailto"))
0208             continue;
0209 
0210         QStringList mail = url.path().split(QLatin1Char('@'));
0211         if (mail.size() != 2)
0212             continue;
0213 
0214         res << qMakePair(Composer::ADDRESS_TO, Imap::Message::MailAddress(QString(), QString(), mail[0], mail[1]));
0215     }
0216 
0217     if (!res.isEmpty()) {
0218         output = deduplicatedAndJustToCcBcc(res);
0219         return true;
0220     }
0221 
0222     return false;
0223 }
0224 
0225 }
0226 
0227 namespace Composer {
0228 namespace Util {
0229 
0230 /** @short Return a list of all addresses which are found in the message's headers */
0231 RecipientList extractListOfRecipients(const QModelIndex &message)
0232 {
0233     Composer::RecipientList originalRecipients;
0234     if (!message.isValid())
0235         return originalRecipients;
0236 
0237     using namespace Imap::Mailbox;
0238     using namespace Imap::Message;
0239     Model *model = dynamic_cast<Model *>(const_cast<QAbstractItemModel *>(message.model()));
0240     TreeItemMessage *messagePtr = dynamic_cast<TreeItemMessage *>(static_cast<TreeItem *>(message.internalPointer()));
0241     Q_ASSERT(messagePtr);
0242     Envelope envelope = messagePtr->envelope(model);
0243 
0244     // Prepare the list of recipients
0245     Q_FOREACH(const MailAddress &addr, envelope.from)
0246         originalRecipients << qMakePair(Composer::ADDRESS_FROM, addr);
0247     Q_FOREACH(const MailAddress &addr, envelope.to)
0248         originalRecipients << qMakePair(Composer::ADDRESS_TO, addr);
0249     Q_FOREACH(const MailAddress &addr, envelope.cc)
0250         originalRecipients << qMakePair(Composer::ADDRESS_CC, addr);
0251     Q_FOREACH(const MailAddress &addr, envelope.bcc)
0252         originalRecipients << qMakePair(Composer::ADDRESS_BCC, addr);
0253     Q_FOREACH(const MailAddress &addr, envelope.sender)
0254         originalRecipients << qMakePair(Composer::ADDRESS_SENDER, addr);
0255     Q_FOREACH(const MailAddress &addr, envelope.replyTo)
0256         originalRecipients << qMakePair(Composer::ADDRESS_REPLY_TO, addr);
0257 
0258     return originalRecipients;
0259 }
0260 
0261 /** @short Prepare a list of recipients to be used when replying
0262 
0263   @return True if the operation is safe and well-defined, false otherwise (there are situations where
0264   one cannot be completely sure that the reply will indeed go just to the original author or when replying
0265   to list is not defined, for example).
0266 
0267 */
0268 bool replyRecipientList(const ReplyMode mode, const SenderIdentitiesModel *senderIdetitiesModel,
0269                         const RecipientList &originalRecipients,
0270                         const QList<QUrl> &headerListPost, const bool headerListPostNo,
0271                         RecipientList &output)
0272 {
0273     switch (mode) {
0274     case REPLY_ALL:
0275         return prepareReplyAll(originalRecipients, output);
0276     case REPLY_ALL_BUT_ME:
0277     {
0278         RecipientList res = output;
0279         bool ok = prepareReplyAll(originalRecipients, res);
0280         if (!ok)
0281             return false;
0282         Q_FOREACH(const Imap::Message::MailAddress &addr, extractEmailAddresses(senderIdetitiesModel)) {
0283             RecipientList::iterator it = res.begin();
0284             while (it != res.end()) {
0285                 if (Imap::Message::MailAddressesEqualByMail(it->second, addr)) {
0286                     // this is our own address
0287                     it = res.erase(it);
0288                 } else {
0289                     ++it;
0290                 }
0291             }
0292         }
0293         // We might have deleted something, let's repeat the Cc -> To (...) promotion
0294         res = deduplicatedAndJustToCcBcc(res);
0295         if (res.size()) {
0296             output = res;
0297             return true;
0298         } else {
0299             return false;
0300         }
0301     }
0302     case REPLY_PRIVATE:
0303         return prepareReplySenderOnly(originalRecipients, headerListPost, output);
0304     case REPLY_LIST:
0305         return prepareReplyList(headerListPost, headerListPostNo, output);
0306     }
0307 
0308     Q_ASSERT(false);
0309     return false;
0310 }
0311 
0312 /** @short Convenience wrapper */
0313 bool replyRecipientList(const ReplyMode mode, const SenderIdentitiesModel *senderIdetitiesModel,
0314                         const QModelIndex &message, RecipientList &output)
0315 {
0316     if (!message.isValid())
0317         return false;
0318 
0319     // Prepare the list of recipients
0320     RecipientList originalRecipients = extractListOfRecipients(message);
0321 
0322     // The List-Post header
0323     QList<QUrl> headerListPost;
0324     Q_FOREACH(const QVariant &item, message.data(Imap::Mailbox::RoleMessageHeaderListPost).toList())
0325         headerListPost << item.toUrl();
0326 
0327     return replyRecipientList(mode, senderIdetitiesModel, originalRecipients, headerListPost,
0328                               message.data(Imap::Mailbox::RoleMessageHeaderListPostNo).toBool(), output);
0329 }
0330 
0331 QList<Imap::Message::MailAddress> extractEmailAddresses(const SenderIdentitiesModel *senderIdetitiesModel)
0332 {
0333     using namespace Imap::Message;
0334     // What identities do we have?
0335     QList<MailAddress> identities;
0336     for (int i = 0; i < senderIdetitiesModel->rowCount(); ++i) {
0337         MailAddress addr;
0338         MailAddress::fromPrettyString(addr,
0339                 senderIdetitiesModel->data(senderIdetitiesModel->index(i, Composer::SenderIdentitiesModel::COLUMN_EMAIL)).toString());
0340         identities << addr;
0341     }
0342     return identities;
0343 }
0344 
0345 /** @short Try to find the preferred identity for a reply looking at a list of recipients */
0346 bool chooseSenderIdentity(const SenderIdentitiesModel *senderIdetitiesModel, const QList<Imap::Message::MailAddress> &addresses, int &row)
0347 {
0348     using namespace Imap::Message;
0349     QList<MailAddress> identities = extractEmailAddresses(senderIdetitiesModel);
0350 
0351     // First of all, look for a full match of the sender among the addresses
0352     for (int i = 0; i < identities.size(); ++i) {
0353         auto it = std::find_if(addresses.constBegin(), addresses.constEnd(),
0354                                [&identities, i](const auto &addr) { return Imap::Message::MailAddressesEqualByMail(addr, identities[i]); });
0355         if (it != addresses.constEnd()) {
0356             // Found an exact match of one of our identities in the recipients -> return that
0357             row = i;
0358             return true;
0359         }
0360     }
0361 
0362     // Then look for the matching domain
0363     for (int i = 0; i < identities.size(); ++i) {
0364         auto it = std::find_if(addresses.constBegin(), addresses.constEnd(),
0365                                [&identities, i](const auto &addr) { return Imap::Message::MailAddressesEqualByDomain(addr, identities[i]); });
0366         if (it != addresses.constEnd()) {
0367             // Found a match because the domain matches -> return that
0368             row = i;
0369             return true;
0370         }
0371     }
0372 
0373     // Check for situations where the identity's domain is the suffix of some address
0374     for (int i = 0; i < identities.size(); ++i) {
0375         auto it = std::find_if(addresses.constBegin(), addresses.constEnd(),
0376                                [&identities, i](const auto &addr) { return Imap::Message::MailAddressesEqualByDomainSuffix(addr, identities[i]); });
0377         if (it != addresses.constEnd()) {
0378             // Found a match because the domain suffix matches -> return that
0379             row = i;
0380             return true;
0381         }
0382     }
0383 
0384     // No other heuristic is there for now -> give up
0385     return false;
0386 }
0387 
0388 /** @short Try to find the preferred indetity for replying to a message */
0389 bool chooseSenderIdentityForReply(const SenderIdentitiesModel *senderIdetitiesModel,
0390                                   const QModelIndex &message, int &row)
0391 {
0392     return chooseSenderIdentity(senderIdetitiesModel, extractEmailAddresses(extractListOfRecipients(message)), row);
0393 }
0394 
0395 /** @short Extract the list of e-mail addresses from the list of <type, address> pairs */
0396 QList<Imap::Message::MailAddress> extractEmailAddresses(const RecipientList &list)
0397 {
0398     QList<Imap::Message::MailAddress> addresses;
0399     std::transform(list.constBegin(), list.constEnd(), std::back_inserter(addresses),
0400                    [](const auto &address) { return address.second; });
0401     return addresses;
0402 }
0403 
0404 }
0405 }