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 }