File indexing completed on 2024-09-22 04:52:49
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 <typeinfo> 0024 0025 #include <QRegularExpression> 0026 #include <QRegularExpressionMatch> 0027 #include <QTextDocument> 0028 #include <QUrl> 0029 #include <QUrlQuery> 0030 #include <QTextCodec> 0031 #include "MailAddress.h" 0032 #include "../Model/MailboxTree.h" 0033 #include "../Encoders.h" 0034 #include "../Parser/Rfc5322HeaderParser.h" 0035 #include "UiUtils/Formatting.h" 0036 0037 namespace Imap 0038 { 0039 namespace Message 0040 { 0041 0042 bool MailAddress::fromPrettyString(MailAddress &into, const QString &address) 0043 { 0044 int offset = 0; 0045 0046 if (!parseOneAddress(into, address, offset)) 0047 return false; 0048 0049 if (offset < address.size()) 0050 return false; 0051 0052 return true; 0053 } 0054 0055 /* Regexpes to match an address typed into the input field. */ 0056 static QRegularExpression mailishRx1(QLatin1String("^\\s*([\\w!#$%&'*+-/=?^_`{|}~]+)\\s*\\@" 0057 "\\s*([\\w_.-]+|(?:\\[[^][\\\\\\\"\\s]+\\]))\\s*$")); 0058 static QRegularExpression mailishRx2(QLatin1String("\\s*<([\\w!#$%&'*+-/=?^_`{|}~]+)\\s*\\@" 0059 "\\s*([\\w_.-]+|(?:\\[[^][\\\\\\\"\\s]+\\]))>\\s*$")); 0060 0061 /* 0062 This is of course far from complete, but at least catches "Real 0063 Name" <foo@bar>. It needs to recognize the things people actually 0064 type, and it should also recognize anything that's a valid 0065 rfc2822 address. 0066 */ 0067 bool MailAddress::parseOneAddress(Imap::Message::MailAddress &into, const QString &address, int &startOffset) 0068 { 0069 for (QRegularExpression mailishRx : {mailishRx2, mailishRx1}) { 0070 QRegularExpressionMatch match = mailishRx.match(address, startOffset); 0071 int offset = match.capturedStart(); 0072 if (match.hasMatch()) { 0073 QString before = address.mid(startOffset, offset - startOffset); 0074 into = MailAddress(before.simplified(), QString(), match.captured(1), match.captured(2)); 0075 0076 offset += match.capturedLength(); 0077 0078 startOffset = offset; 0079 return true; 0080 } 0081 } 0082 return false; 0083 } 0084 0085 MailAddress::MailAddress(const QVariantList &input, const QByteArray &line, const int start) 0086 { 0087 // FIXME: all offsets are wrong here 0088 if (input.size() != 4) 0089 throw ParseError("MailAddress: not four items", line, start); 0090 0091 if (input[0].type() != QVariant::ByteArray) 0092 throw UnexpectedHere("MailAddress: item#1 not a QByteArray", line, start); 0093 if (input[1].type() != QVariant::ByteArray) 0094 throw UnexpectedHere("MailAddress: item#2 not a QByteArray", line, start); 0095 if (input[2].type() != QVariant::ByteArray) 0096 throw UnexpectedHere("MailAddress: item#3 not a QByteArray", line, start); 0097 if (input[3].type() != QVariant::ByteArray) 0098 throw UnexpectedHere("MailAddress: item#4 not a QByteArray", line, start); 0099 0100 name = Imap::decodeRFC2047String(input[0].toByteArray()); 0101 adl = Imap::decodeRFC2047String(input[1].toByteArray()); 0102 mailbox = Imap::decodeRFC2047String(input[2].toByteArray()); 0103 host = Imap::decodeRFC2047String(input[3].toByteArray()); 0104 } 0105 0106 QUrl MailAddress::asUrl() const 0107 { 0108 QUrl url; 0109 url.setScheme(QStringLiteral("mailto")); 0110 url.setPath(QStringLiteral("%1@%2").arg(mailbox, host)); 0111 if (!name.isEmpty()) { 0112 QUrlQuery q(url); 0113 q.addQueryItem(QStringLiteral("X-Trojita-DisplayName"), name); 0114 url.setQuery(q); 0115 } 0116 return url; 0117 } 0118 0119 QString MailAddress::prettyName(FormattingMode mode) const 0120 { 0121 bool hasNiceName = !name.isEmpty(); 0122 0123 if (!hasNiceName && mode == FORMAT_JUST_NAME) 0124 mode = FORMAT_READABLE; 0125 0126 if (mode == FORMAT_JUST_NAME) { 0127 return name; 0128 } else { 0129 QString address = mailbox + QLatin1Char('@') + host; 0130 QString niceName; 0131 if (hasNiceName) { 0132 niceName = name; 0133 } else { 0134 niceName = address; 0135 } 0136 if (mode == FORMAT_READABLE) { 0137 if (hasNiceName) { 0138 return name + QLatin1String(" <") + address + QLatin1Char('>'); 0139 } else { 0140 return address; 0141 } 0142 } else { 0143 if (mode == FORMAT_SHORT_CLICKABLE) 0144 UiUtils::elideAddress(niceName); 0145 return QStringLiteral("<a href=\"%1\">%2</a>").arg(asUrl().toString().toHtmlEscaped(), niceName.toHtmlEscaped()); 0146 } 0147 } 0148 } 0149 0150 QString MailAddress::prettyList(const QList<MailAddress> &list, FormattingMode mode) 0151 { 0152 QStringList buf; 0153 for (QList<MailAddress>::const_iterator it = list.begin(); it != list.end(); ++it) 0154 buf << it->prettyName(mode); 0155 return buf.join(QStringLiteral(", ")); 0156 } 0157 0158 QString MailAddress::prettyList(const QVariantList &list, FormattingMode mode) 0159 { 0160 QStringList buf; 0161 for (QVariantList::const_iterator it = list.begin(); it != list.end(); ++it) { 0162 Q_ASSERT(it->type() == QVariant::StringList); 0163 QStringList item = it->toStringList(); 0164 Q_ASSERT(item.size() == 4); 0165 MailAddress a(item[0], item[1], item[2], item[3]); 0166 buf << a.prettyName(mode); 0167 } 0168 return buf.join(QStringLiteral(", ")); 0169 } 0170 0171 static QRegularExpression dotAtomRx(QLatin1String("^[A-Za-z0-9!#$&'*+/=?^_`{}|~-]+(?:\\.[A-Za-z0-9!#$&'*+/=?^_`{}|~-]+)*$")); 0172 0173 /* This returns the address formatted for use in an SMTP MAIL or RCPT command; specifically, it matches the "Mailbox" production of RFC2821. The surrounding angle-brackets are not included. */ 0174 QByteArray MailAddress::asSMTPMailbox() const 0175 { 0176 QByteArray result; 0177 0178 /* Check whether the local-part contains any characters 0179 preventing it from being a dot-atom. */ 0180 if (dotAtomRx.match(mailbox).hasMatch()) { 0181 /* Using .toLatin1() here even though we know it only contains 0182 ASCII, because QString.toAscii() does not necessarily convert 0183 to ASCII (despite the name). .toLatin1() always converts to 0184 Latin-1. */ 0185 result = mailbox.toLatin1(); 0186 } else { 0187 /* The other syntax allowed for local-parts is a double-quoted string. 0188 Note that RFC2047 tokens aren't allowed there --- local-parts are 0189 fundamentally bytestrings, apparently, whose interpretation is 0190 up to the receiving system. If someone types non-ASCII characters 0191 into the address field we'll generate non-conforming headers, but 0192 it's the best we can do. */ 0193 result = Imap::quotedString(mailbox.toUtf8()); 0194 } 0195 0196 result.append("@"); 0197 0198 QByteArray domainpart; 0199 0200 if (!(host.startsWith(QLatin1Char('[')) || host.endsWith(QLatin1Char(']')))) { 0201 /* IDN-encode the hostname part of the address */ 0202 domainpart = QUrl::toAce(host); 0203 0204 /* TODO: QUrl::toAce() is documented to return an empty result if 0205 the string isn't a valid hostname --- for example, if it's a 0206 domain literal containing an IP address. In that case, we'll 0207 need to encode it ourselves (making sure there are square 0208 brackets, no forbidden characters, appropriate backslashes, and so on). */ 0209 } 0210 0211 if (domainpart.isEmpty()) { 0212 /* Either the domainpart looks like a domain-literal, or toAce() failed. */ 0213 0214 domainpart = host.toUtf8(); 0215 if (domainpart.startsWith('[')) { 0216 domainpart.remove(0, 1); 0217 } 0218 if (domainpart.endsWith(']')) { 0219 domainpart.remove(domainpart.size()-1, 1); 0220 } 0221 0222 result.append(Imap::quotedString(domainpart, Imap::SquareBrackets)); 0223 } else { 0224 result.append(domainpart); 0225 } 0226 0227 return result; 0228 } 0229 0230 QByteArray MailAddress::asMailHeader() const 0231 { 0232 QByteArray result = Imap::encodeRFC2047Phrase(name); 0233 0234 if (!result.isEmpty()) 0235 result.append(" "); 0236 0237 result.append("<"); 0238 result.append(asSMTPMailbox()); 0239 result.append(">"); 0240 0241 return result; 0242 } 0243 0244 /** @short The mail address usable for manipulation by user */ 0245 QString MailAddress::asPrettyString() const 0246 { 0247 return name.isEmpty() ? 0248 QString::fromUtf8(asSMTPMailbox()) : 0249 name + QLatin1Char(' ') + QLatin1Char('<') + QString::fromUtf8(asSMTPMailbox()) + QLatin1Char('>'); 0250 } 0251 0252 /** @short Is the human-readable part "useful", i.e. does it contain something else besides the e-mail address? */ 0253 bool MailAddress::hasUsefulDisplayName() const 0254 { 0255 return !name.isEmpty() && name.trimmed().toUtf8().toLower() != asSMTPMailbox().toLower(); 0256 } 0257 0258 /** @short Convert a QUrl into a MailAddress instance */ 0259 bool MailAddress::fromUrl(MailAddress &into, const QUrl &url, const QString &expectedScheme) 0260 { 0261 if (url.scheme().toLower() != expectedScheme.toLower()) 0262 return false; 0263 0264 QStringList list = url.path().split(QLatin1Char('@')); 0265 if (list.size() != 2) 0266 return false; 0267 0268 QUrlQuery q(url); 0269 Imap::Message::MailAddress addr(q.queryItemValue(QStringLiteral("X-Trojita-DisplayName")), QString(), 0270 list[0], list[1]); 0271 0272 if (!addr.hasUsefulDisplayName()) 0273 addr.name.clear(); 0274 into = addr; 0275 return true; 0276 } 0277 0278 /** @short Helper to construct this from a pair of (human readable name, e-mail address) 0279 0280 This is mainly useful to prevent reimplementing the @-based joining all the time. 0281 */ 0282 MailAddress MailAddress::fromNameAndMail(const QString &name, const QString &email) 0283 { 0284 auto components = email.split(QLatin1Char('@')); 0285 if (components.size() == 2) { 0286 return MailAddress(name, QString(), components[0], components[1]); 0287 } else { 0288 // garbage in, garbage out 0289 return MailAddress(name, QString(), email, QString()); 0290 } 0291 } 0292 0293 QTextStream &operator<<(QTextStream &stream, const MailAddress &address) 0294 { 0295 stream << '"' << address.name << "\" <"; 0296 if (!address.host.isNull()) 0297 stream << address.mailbox << '@' << address.host; 0298 else 0299 stream << address.mailbox; 0300 stream << '>'; 0301 return stream; 0302 } 0303 0304 bool operator==(const MailAddress &a, const MailAddress &b) 0305 { 0306 return a.name == b.name && a.adl == b.adl && a.mailbox == b.mailbox && a.host == b.host; 0307 } 0308 0309 bool MailAddressesEqualByMail(const MailAddress &a, const MailAddress &b) 0310 { 0311 // FIXME: fancy stuff like the IDN? 0312 return a.mailbox.toLower() == b.mailbox.toLower() && a.host.toLower() == b.host.toLower(); 0313 } 0314 0315 bool MailAddressesEqualByDomain(const MailAddress &a, const MailAddress &b) 0316 { 0317 // FIXME: fancy stuff like the IDN? 0318 return a.host.toLower() == b.host.toLower(); 0319 } 0320 0321 0322 bool MailAddressesEqualByDomainSuffix(const MailAddress &a, const MailAddress &b) 0323 { 0324 // FIXME: fancy stuff like the IDN? 0325 auto aHost = a.host.toLower(); 0326 auto bHost = b.host.toLower(); 0327 return aHost == bHost || aHost.endsWith(QLatin1Char('.') + bHost); 0328 } 0329 0330 } 0331 } 0332 0333 QDataStream &operator>>(QDataStream &stream, Imap::Message::MailAddress &a) 0334 { 0335 return stream >> a.adl >> a.host >> a.mailbox >> a.name; 0336 } 0337 0338 QDataStream &operator<<(QDataStream &stream, const Imap::Message::MailAddress &a) 0339 { 0340 return stream << a.adl << a.host << a.mailbox << a.name; 0341 }