1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
3 This file is part of the Trojita Qt IMAP e-mail client,
4 http://trojita.flaska.net/
6 This program is free software; you can redistribute it and/or
7 modify it under the terms of the GNU General Public License as
8 published by the Free Software Foundation; either version 2 of
9 the License or (at your option) version 3 or any later version
10 accepted by the membership of KDE e.V. (or its successor approved
11 by the membership of KDE e.V.), which shall act as a proxy
12 defined in Section 14 of version 3 of the license.
14 This program is distributed in the hope that it will be useful,
15 but WITHOUT ANY WARRANTY; without even the implied warranty of
16 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 GNU General Public License for more details.
19 You should have received a copy of the GNU General Public License
20 along with this program. If not, see <http://www.gnu.org/licenses/>.
22 #include "UiUtils/Formatting.h"
26 #include <QStringList>
27 #include <QTextDocument>
29 #include "UiUtils/PlainTextFormatter.h"
33 Formatting::Formatting(QObject
*parent
): QObject(parent
)
37 QString
Formatting::prettySize(quint64 bytes
)
39 static const QStringList suffixes
= QStringList() << tr("B")
44 const int max_order
= suffixes
.size() - 1;
45 double number
= bytes
;
49 int magnitude
= std::log10(number
);
50 number
/= std::pow(10.0, magnitude
); // x.yz... * 10^magnitude
51 number
= qRound(number
* 100.0) / 100.0; // round to 3 significant digits
52 if (number
>= 10.0) { // rounding has caused one increase in magnitude
56 order
= magnitude
/ 3;
57 int rem
= magnitude
% 3;
58 number
*= std::pow(10.0, rem
); // xy.z * 1000^order
59 if (order
<= max_order
) {
60 frac_digits
= 2 - rem
;
61 } else { // shame on you for such large mails
62 number
*= std::pow(1000.0, order
- max_order
);
66 return tr("%1 %2").arg(QString::number(number
, 'f', frac_digits
), suffixes
.at(order
));
69 /** @short Format a QDateTime for compact display in one column of the view */
70 QString
Formatting::prettyDate(const QDateTime
&dateTime
)
72 // The time is not always synced properly, so better accept even slightly too new messages as "from today"
73 QDateTime now
= QDateTime::currentDateTime().addSecs(15*60);
74 if (dateTime
>= now
) {
75 // Messages from future shall always be shown using full format to prevent nasty surprises.
76 return dateTime
.toString(Qt::DefaultLocaleShortDate
);
77 } else if (dateTime
.date() == now
.date() || dateTime
> now
.addSecs(-6 * 3600)) {
78 // It's a "today's message", i.e. something which is either literally from today or at least something not older than
79 // six hours (an arbitraty magic number).
80 // Originally, the cut-off time interval was set to 24 hours, but it led to weird things in the GUI like showing mails
81 // from yesterday's 18:33 just as "18:33" even though the local time was "18:20" already. In a perfect world, we would
82 // also periodically emit dataChanged() in order to force a wrap once the view has been open for too long, but that will
83 // have to wait a bit.
84 // The time is displayed without seconds to conserve space as well.
85 return dateTime
.time().toString(tr("hh:mm", "Please do not translate the format specifiers. "
86 "You can change their order or the separator to follow the local conventions. "
87 "For valid specifiers see http://doc.qt.io/qt-5/qdatetime.html#toString"));
88 } else if (dateTime
> now
.addDays(-7)) {
89 // Messages from the last seven days can be formatted just with the weekday name
90 return dateTime
.toString(tr("ddd hh:mm", "Please do not translate the format specifiers. "
91 "You can change their order or the separator to follow the local conventions. "
92 "For valid specifiers see http://doc.qt.io/qt-5/qdatetime.html#toString"));
93 } else if (dateTime
.date().year() == now
.date().year() || dateTime
> now
.addMonths(-6)) {
94 // Originally, this used to handle messages fresher than an year old. However, this might get a wee bit confusing
95 // when it's September and we're showing a message from November last year.
96 // I think that it's OK-ish to assume that unchanged years are OK, but let's be more careful when crossing the year
97 // boundary. This is just a number that I pulled out of my sleeve, but a six-month cutoff might do the trick here.
98 return dateTime
.toString(tr("d MMM hh:mm", "Please do not translate the format specifiers. "
99 "You can change their order or the separator to follow the local conventions. "
100 "For valid specifiers see http://doc.qt.io/qt-5/qdatetime.html#toString"));
102 // Old messagees shall have a full date
103 return dateTime
.toString(Qt::DefaultLocaleShortDate
);
107 QString
Formatting::htmlizedTextPart(const QModelIndex
&partIndex
, const QFont
&font
,
108 const QColor
&backgroundColor
, const QColor
&textColor
,
109 const QColor
&linkColor
, const QColor
&visitedLinkColor
)
111 Q_ASSERT(partIndex
.isValid());
112 QFontInfo
fontInfo(font
);
113 return UiUtils::htmlizedTextPart(partIndex
, fontInfo
,
114 backgroundColor
, textColor
,
115 linkColor
, visitedLinkColor
);
118 /** @short Produce a properly formatted HTML string which won't overflow the right edge of the display */
119 QString
Formatting::htmlHexifyByteArray(const QByteArray
&rawInput
)
121 QByteArray inHex
= rawInput
.toHex();
123 const int stepping
= 4;
124 for (int i
= 0; i
< inHex
.length(); i
+= stepping
) {
125 // The individual blocks are formatted separately to allow line breaks to happen
126 res
.append("<code style=\"font-family: monospace;\">");
127 res
.append(inHex
.mid(i
, stepping
));
128 if (i
+ stepping
< inHex
.size()) {
131 // Produce the smallest possible space. "display: none" won't notice the space at all, leading to overly long lines
132 res
.append("</code><span style=\"font-size: 1px\"> </span>");
134 return QString::fromUtf8(res
);
137 QString
Formatting::sslChainToHtml(const QList
<QSslCertificate
> &sslChain
)
139 QStringList certificateStrings
;
140 Q_FOREACH(const QSslCertificate
&cert
, sslChain
) {
141 certificateStrings
<< tr("<li><b>CN</b>: %1,<br/>\n<b>Organization</b>: %2,<br/>\n"
142 "<b>Serial</b>: %3,<br/>\n"
143 "<b>SHA1</b>: %4,<br/>\n<b>MD5</b>: %5</li>").arg(
144 cert
.subjectInfo(QSslCertificate::CommonName
).join(tr(", ")).toHtmlEscaped(),
145 cert
.subjectInfo(QSslCertificate::Organization
).join(tr(", ")).toHtmlEscaped(),
146 QString::fromUtf8(cert
.serialNumber()),
147 htmlHexifyByteArray(cert
.digest(QCryptographicHash::Sha1
)),
148 htmlHexifyByteArray(cert
.digest(QCryptographicHash::Md5
)));
150 return sslChain
.isEmpty() ?
151 tr("<p>The remote side doesn't have a certificate.</p>\n") :
152 tr("<p>This is the certificate chain of the connection:</p>\n<ul>%1</ul>\n").arg(certificateStrings
.join(tr("\n")));
155 QString
Formatting::sslErrorsToHtml(const QList
<QSslError
> &sslErrors
)
157 QStringList sslErrorStrings
;
158 Q_FOREACH(const QSslError
&e
, sslErrors
) {
159 sslErrorStrings
<< tr("<li>%1</li>").arg(e
.errorString().toHtmlEscaped());
161 return sslErrors
.isEmpty() ?
162 tr("<p>According to your system's policy, this connection is secure.</p>\n") :
163 tr("<p>The connection triggered the following SSL errors:</p>\n<ul>%1</ul>\n").arg(sslErrorStrings
.join(tr("\n")));
166 void Formatting::formatSslState(const QList
<QSslCertificate
> &sslChain
, const QByteArray
&oldPubKey
,
167 const QList
<QSslError
> &sslErrors
, QString
*title
, QString
*message
, IconType
*icon
)
169 bool pubKeyHasChanged
= !oldPubKey
.isEmpty() && (sslChain
.isEmpty() || sslChain
[0].publicKey().toPem() != oldPubKey
);
171 if (pubKeyHasChanged
) {
172 if (sslErrors
.isEmpty()) {
173 *icon
= IconType::Warning
;
174 *title
= tr("Different SSL certificate");
175 *message
= tr("<p>The public key of the SSL certificate has changed. "
176 "This should only happen when there was a security incident on the remote server. "
177 "Your system configuration is set to accept such certificates anyway.</p>\n%1\n"
178 "<p>Would you like to connect and remember the new certificate?</p>")
179 .arg(sslChainToHtml(sslChain
));
181 // changed certificate which is not trusted per systemwide policy
182 *title
= tr("SSL certificate looks fishy");
183 *message
= tr("<p>The public key of the SSL certificate of the IMAP server has changed since the last time "
184 "and your system doesn't believe that the new certificate is genuine.</p>\n%1\n%2\n"
185 "<p>Would you like to connect anyway and remember the new certificate?</p>").
186 arg(sslChainToHtml(sslChain
), sslErrorsToHtml(sslErrors
));
187 *icon
= IconType::Critical
;
190 if (sslErrors
.isEmpty()) {
191 // this is the first time and the certificate looks valid -> accept
192 *title
= tr("Accept SSL connection?");
193 *message
= tr("<p>This is the first time you're connecting to this IMAP server; the certificate is trusted "
194 "by this system.</p>\n%1\n%2\n"
195 "<p>Would you like to connect and remember this certificate's public key for the next time?</p>")
196 .arg(sslChainToHtml(sslChain
), sslErrorsToHtml(sslErrors
));
197 *icon
= IconType::Information
;
199 *title
= tr("Accept SSL connection?");
200 *message
= tr("<p>This is the first time you're connecting to this IMAP server and the server certificate failed "
201 "validation test.</p>\n%1\n\n%2\n"
202 "<p>Would you like to connect and remember this certificate's public key for the next time?</p>")
203 .arg(sslChainToHtml(sslChain
), sslErrorsToHtml(sslErrors
));
204 *icon
= IconType::Question
;
209 /** @short Input formatted as HTML with proper escaping and forced to be detected as HTML */
210 QString
Formatting::htmlEscaped(const QString
&input
)
215 // HTML entities are escaped, but not auto-detected as HTML
216 return QLatin1String("<span>") + input
.toHtmlEscaped() + QLatin1String("</span>");
219 QObject
*Formatting::factory(QQmlEngine
*engine
, QJSEngine
*scriptEngine
)
221 Q_UNUSED(scriptEngine
);
223 // the reinterpret_cast is used to avoid haivng to depend on QtQuick when doing non-QML builds
224 Formatting
*f
= new Formatting(reinterpret_cast<QObject
*>(engine
));
228 bool elideAddress(QString
&address
)
230 if (address
.length() < 66)
233 const int idx
= address
.lastIndexOf(QLatin1Char('@'));
234 auto ellipsis
= QStringLiteral("\u2026");
236 if (idx
< 9) // local part is too short to strip anything
239 // do not stash the domain and leave at least 4 chars head and tail of the local part
240 const int d
= qMax(8, idx
- (address
.length() - 60))/2;
241 address
= address
.leftRef(d
) + ellipsis
+ address
.rightRef(address
.length() - idx
+ d
);
243 // some longer something, just remove the overhead in the center to eg.
244 // leave "https://" and "foo/index.html" intact
245 address
= address
.leftRef(30) + ellipsis
+ address
.rightRef(30);