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 }