messageviewerutil.cpp 31.8 KB
Newer Older
Laurent Montel's avatar
Laurent Montel committed
1
/*******************************************************************************
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
**
** Filename   : util
** Created on : 03 April, 2005
** Copyright  : (c) 2005 Till Adam
** Email      : <adam@kde.org>
**
*******************************************************************************/

/*******************************************************************************
**
**   This program is free software; you can redistribute it and/or modify
**   it under the terms of the GNU General Public License as published by
**   the Free Software Foundation; either version 2 of the License, or
**   (at your option) any later version.
**
**   It is distributed in the hope that it will be useful, but
**   WITHOUT ANY WARRANTY; without even the implied warranty of
**   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
**   General Public License for more details.
**
**   You should have received a copy of the GNU General Public License
**   along with this program; if not, write to the Free Software
**   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
**   In addition, as a special exception, the copyright holders give
**   permission to link the code of this program with any edition of
**   the Qt library by Trolltech AS, Norway (or with modified versions
**   of Qt that use the same license as Qt), and distribute linked
**   combinations including the two.  You must obey the GNU General
**   Public License in all respects for all of the code used other than
**   Qt.  If you modify this file, you may extend this exception to
**   your version of the file, but you are not obligated to do so.  If
**   you do not wish to do so, delete this exception statement from
**   your version.
**
*******************************************************************************/

#include "messageviewer/messageviewerutil.h"
40
#include "messageviewerutil_p.h"
Laurent Montel's avatar
Laurent Montel committed
41
#include <KPIMTextEdit/TextToSpeech>
42
#include <MimeTreeParser/NodeHelper>
43
44
45
46
#include "messageviewer_debug.h"
#include "MessageCore/MessageCoreSettings"
#include "MessageCore/NodeHelper"
#include "MessageCore/StringUtil"
47
#include <kio_version.h>
48

Laurent Montel's avatar
Laurent Montel committed
49
#include <PimCommon/RenameFileDialog>
50

Laurent Montel's avatar
Laurent Montel committed
51
52
53
#include <Gravatar/GravatarCache>
#include <gravatar/gravatarsettings.h>

54
55
#include <AkonadiCore/item.h>

56
#include <KMbox/MBox>
57
58

#include <KMime/Message>
59
#include <KFileWidget>
60
#include <KCharsets>
61
#include <KLocalizedString>
62
#include <KMessageBox>
63
64
65
66
67
#include <QFileDialog>
#include <ktoolinvocation.h>
#include <KJobWidgets>
#include <KIO/StatJob>
#include <KIO/FileCopyJob>
68
#include <KRecentDirs>
69
70
71
72
73
74
#include <QAction>
#include <QIcon>
#include <QTemporaryFile>
#include <QWidget>
#include <QDBusConnectionInterface>
#include <QActionGroup>
75
#include <QDesktopServices>
76
#include <QRegularExpression>
77

78
79
#include <qtwebenginewidgetsversion.h>

80
using namespace MessageViewer;
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
/** Checks whether @p str contains external references. To be precise,
    we only check whether @p str contains 'xxx="http[s]:' where xxx is
    not href. Obfuscated external references are ignored on purpose.
*/

bool Util::containsExternalReferences(const QString &str, const QString &extraHead)
{
    const bool hasBaseInHeader = extraHead.contains(QLatin1String(
                                                        "<base href=\""), Qt::CaseInsensitive);
    if (hasBaseInHeader && (str.contains(QLatin1String("href=\"/"), Qt::CaseInsensitive)
                            || str.contains(QLatin1String("<img src=\"/"), Qt::CaseInsensitive))) {
        return true;
    }
    int httpPos = str.indexOf(QLatin1String("\"http:"), Qt::CaseInsensitive);
    int httpsPos = str.indexOf(QLatin1String("\"https:"), Qt::CaseInsensitive);
    while (httpPos >= 0 || httpsPos >= 0) {
        // pos = index of next occurrence of "http: or "https: whichever comes first
        int pos = (httpPos < httpsPos)
Laurent Montel's avatar
Laurent Montel committed
99
100
                  ? ((httpPos >= 0) ? httpPos : httpsPos)
                  : ((httpsPos >= 0) ? httpsPos : httpPos);
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
        // look backwards for "href"
        if (pos > 5) {
            int hrefPos = str.lastIndexOf(QLatin1String("href"), pos - 5, Qt::CaseInsensitive);
            // if no 'href' is found or the distance between 'href' and '"http[s]:'
            // is larger than 7 (7 is the distance in 'href = "http[s]:') then
            // we assume that we have found an external reference
            if ((hrefPos == -1) || (pos - hrefPos > 7)) {
                // HTML messages created by KMail itself for now contain the following:
                // <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
                // Make sure not to show an external references warning for this string
                int dtdPos = str.indexOf(QLatin1String(
                                             "http://www.w3.org/TR/html4/loose.dtd"), pos + 1);
                if (dtdPos != (pos + 1)) {
                    return true;
                }
            }
        }
        // find next occurrence of "http: or "https:
        if (pos == httpPos) {
            httpPos = str.indexOf(QLatin1String("\"http:"), httpPos + 6, Qt::CaseInsensitive);
        } else {
            httpsPos = str.indexOf(QLatin1String("\"https:"), httpsPos + 7, Qt::CaseInsensitive);
        }
    }
125
126
    QRegularExpressionMatch rmatch;

127
128
129
130
131
132
133
134
135
136
137
138
139
    const int startImgIndex = str.indexOf(QLatin1String("<img "));
    QString newStringImg;
    if (startImgIndex != -1) {
        for (int i = startImgIndex, total = str.length(); i < total; ++i) {
            const QChar charStr = str.at(i);
            if (charStr == QLatin1Char('>')) {
                newStringImg += charStr;
                break;
            } else {
                newStringImg += charStr;
            }
        }
        if (!newStringImg.isEmpty()) {
Laurent Montel's avatar
Laurent Montel committed
140
            const bool containsReg2 = newStringImg.contains(QRegularExpression(QStringLiteral("<img.*src=\"https?:/.*\".*>"), QRegularExpression::CaseInsensitiveOption), &rmatch);
141
            if (!containsReg2) {
Laurent Montel's avatar
Laurent Montel committed
142
                const bool containsReg = newStringImg.contains(QRegularExpression(QStringLiteral("<img.*src=https?:/.*>"), QRegularExpression::CaseInsensitiveOption), &rmatch);
143
144
145
146
147
148
149
                return containsReg;
            } else {
                return true;
            }
        }
    }
    return false;
150
}
151
152
153
154
155
156
157

bool Util::checkOverwrite(const QUrl &url, QWidget *w)
{
    bool fileExists = false;
    if (url.isLocalFile()) {
        fileExists = QFile::exists(url.toLocalFile());
    } else {
158
159
160
#if KIO_VERSION < QT_VERSION_CHECK(5, 69, 0)
        auto job = KIO::stat(url, KIO::StatJob::DestinationSide, 0, KIO::JobFlag::DefaultFlags);
#else
Luca Beltrame's avatar
Luca Beltrame committed
161
        auto job = KIO::statDetails(url, KIO::StatJob::DestinationSide, KIO::StatBasic);
162
#endif
163
164
165
166
167
        KJobWidgets::setWindow(job, w);
        fileExists = job->exec();
    }
    if (fileExists) {
        if (KMessageBox::Cancel == KMessageBox::warningContinueCancel(
Laurent Montel's avatar
Laurent Montel committed
168
169
170
171
172
                w,
                i18n("A file named \"%1\" already exists. "
                     "Are you sure you want to overwrite it?", url.toDisplayString()),
                i18n("Overwrite File?"),
                KStandardGuiItem::overwrite())) {
173
174
175
176
177
178
179
180
181
182
183
184
            return false;
        }
    }
    return true;
}

bool Util::handleUrlWithQDesktopServices(const QUrl &url)
{
#if defined Q_OS_WIN || defined Q_OS_MACX
    QDesktopServices::openUrl(url);
    return true;
#else
185
186
187
188
189
    // Always handle help through khelpcenter or browser
    if (url.scheme() == QLatin1String("help")) {
        QDesktopServices::openUrl(url);
        return true;
    }
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
    return false;
#endif
}

KMime::Content::List Util::allContents(const KMime::Content *message)
{
    KMime::Content::List result;
    KMime::Content *child = MessageCore::NodeHelper::firstChild(message);
    if (child) {
        result += child;
        result += allContents(child);
    }
    KMime::Content *next = MessageCore::NodeHelper::nextSibling(message);
    if (next) {
        result += next;
        result += allContents(next);
    }

    return result;
}

Laurent Montel's avatar
Laurent Montel committed
211
bool Util::saveContents(QWidget *parent, const KMime::Content::List &contents, QList<QUrl> &urlList)
212
213
{
    QUrl url, dirUrl;
Laurent Montel's avatar
Laurent Montel committed
214
    QString recentDirClass;
Laurent Montel's avatar
Laurent Montel committed
215
    QUrl currentFolder;
216
217
218
    const bool multiple = (contents.count() > 1);
    if (multiple) {
        // get the dir
Laurent Montel's avatar
Laurent Montel committed
219
220
221
222
223
        dirUrl = QFileDialog::getExistingDirectoryUrl(parent, i18n(
                                                          "Save Attachments To"),
                                                      KFileWidget::getStartUrl(QUrl(QStringLiteral(
                                                                                        "kfiledialog:///attachmentDir")),
                                                                               recentDirClass));
224
225
226
227
228
229
230
231
232
233
234
235
        if (!dirUrl.isValid()) {
            return false;
        }

        // we may not get a slash-terminated url out of KFileDialog
        if (!dirUrl.path().endsWith(QLatin1Char('/'))) {
            dirUrl.setPath(dirUrl.path() + QLatin1Char('/'));
        }
        currentFolder = dirUrl;
    } else {
        // only one item, get the desired filename
        KMime::Content *content = contents.first();
236
        QString fileName = MimeTreeParser::NodeHelper::fileName(content);
237
238
239
240
        fileName = MessageCore::StringUtil::cleanFileName(fileName);
        if (fileName.isEmpty()) {
            fileName = i18nc("filename for an unnamed attachment", "attachment.1");
        }
Laurent Montel's avatar
Laurent Montel committed
241

Laurent Montel's avatar
Laurent Montel committed
242
243
244
        QUrl localUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral(
                                                          "kfiledialog:///attachmentDir")),
                                                 recentDirClass);
Laurent Montel's avatar
Laurent Montel committed
245
        localUrl.setPath(localUrl.path() + QLatin1Char('/') + fileName);
246
        QFileDialog::Options options = QFileDialog::DontConfirmOverwrite;
Laurent Montel's avatar
Laurent Montel committed
247
248
        url = QFileDialog::getSaveFileUrl(parent, i18n("Save Attachment"), localUrl,
                                          QString(), nullptr, options);
249
250
251
252
253
        if (url.isEmpty()) {
            return false;
        }
        currentFolder = KIO::upUrl(url);
    }
Laurent Montel's avatar
Laurent Montel committed
254
255
256
257
258

    if (!recentDirClass.isEmpty()) {
        KRecentDirs::add(recentDirClass, currentFolder.path());
    }

259
260
261
262
    QMap< QString, int > renameNumbering;

    bool globalResult = true;
    int unnamedAtmCount = 0;
Laurent Montel's avatar
Laurent Montel committed
263
264
    PimCommon::RenameFileDialog::RenameFileDialogResult result
        = PimCommon::RenameFileDialog::RENAMEFILE_IGNORE;
Laurent Montel's avatar
Laurent Montel committed
265
    for (KMime::Content *content : qAsConst(contents)) {
266
267
268
        QUrl curUrl;
        if (!dirUrl.isEmpty()) {
            curUrl = dirUrl;
269
            QString fileName = MimeTreeParser::NodeHelper::fileName(content);
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
            fileName = MessageCore::StringUtil::cleanFileName(fileName);
            if (fileName.isEmpty()) {
                ++unnamedAtmCount;
                fileName = i18nc("filename for the %1-th unnamed attachment",
                                 "attachment.%1", unnamedAtmCount);
            }
            if (!curUrl.path().endsWith(QLatin1Char('/'))) {
                curUrl.setPath(curUrl.path() + QLatin1Char('/'));
            }
            curUrl.setPath(curUrl.path() + fileName);
        } else {
            curUrl = url;
        }
        if (!curUrl.isEmpty()) {
            //Bug #312954
            if (multiple && (curUrl.fileName() == QLatin1String("smime.p7s"))) {
                continue;
            }
            // Rename the file if we have already saved one with the same name:
            // try appending a number before extension (e.g. "pic.jpg" => "pic_2.jpg")
            QString origFile = curUrl.fileName();
            QString file = origFile;

            while (renameNumbering.contains(file)) {
                file = origFile;
                int num = renameNumbering[file] + 1;
                int dotIdx = file.lastIndexOf(QLatin1Char('.'));
297
                file.insert((dotIdx >= 0) ? dotIdx : file.length(), QLatin1Char(
Laurent Montel's avatar
Laurent Montel committed
298
                                '_') + QString::number(num));
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
            }
            curUrl = curUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
            curUrl.setPath(curUrl.path() + QLatin1Char('/') + file);

            // Increment the counter for both the old and the new filename
            if (!renameNumbering.contains(origFile)) {
                renameNumbering[origFile] = 1;
            } else {
                renameNumbering[origFile]++;
            }

            if (file != origFile) {
                if (!renameNumbering.contains(file)) {
                    renameNumbering[file] = 1;
                } else {
                    renameNumbering[file]++;
                }
            }

Laurent Montel's avatar
Laurent Montel committed
318
319
            if (!(result == PimCommon::RenameFileDialog::RENAMEFILE_OVERWRITEALL
                  || result == PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL)) {
320
321
322
323
                bool fileExists = false;
                if (curUrl.isLocalFile()) {
                    fileExists = QFile::exists(curUrl.toLocalFile());
                } else {
324
#if KIO_VERSION < QT_VERSION_CHECK(5, 69, 0)
325
                    auto job = KIO::stat(curUrl, KIO::StatJob::DestinationSide, 0);
326
#else
Laurent Montel's avatar
Laurent Montel committed
327
                    auto job = KIO::statDetails(url, KIO::StatJob::DestinationSide, KIO::StatDetail::StatBasic);
328
#endif
329
330
331
332
                    KJobWidgets::setWindow(job, parent);
                    fileExists = job->exec();
                }
                if (fileExists) {
Laurent Montel's avatar
Laurent Montel committed
333
334
335
336
                    QPointer<PimCommon::RenameFileDialog> dlg = new PimCommon::RenameFileDialog(
                        curUrl,
                        multiple,
                        parent);
Laurent Montel's avatar
Laurent Montel committed
337
338
339
340
                    result
                        = static_cast<PimCommon::RenameFileDialog::RenameFileDialogResult>(dlg->exec());
                    if (result == PimCommon::RenameFileDialog::RENAMEFILE_IGNORE
                        || result == PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL) {
341
342
343
                        delete dlg;
                        continue;
                    } else if (result == PimCommon::RenameFileDialog::RENAMEFILE_RENAME) {
Laurent Montel's avatar
Laurent Montel committed
344
345
346
                        if (dlg) {
                            curUrl = dlg->newName();
                        }
347
348
349
350
351
352
                    }
                    delete dlg;
                }
            }
            // save
            if (result != PimCommon::RenameFileDialog::RENAMEFILE_IGNOREALL) {
353
354
355
                const bool resultSave = saveContent(parent, content, curUrl);
                if (!resultSave) {
                    globalResult = resultSave;
356
357
                } else {
                    urlList.append(curUrl);
358
359
360
361
362
363
364
365
366
367
                }
            }
        }
    }

    return globalResult;
}

bool Util::saveContent(QWidget *parent, KMime::Content *content, const QUrl &url)
{
Yuri Chornoivan's avatar
Yuri Chornoivan committed
368
    // FIXME: This is all horribly broken. First of all, creating a NodeHelper and then immediately
369
370
371
372
373
374
    //        reading out the encryption/signature state will not work at all.
    //        Then, topLevel() will not work for attachments that are inside encrypted parts.
    //        What should actually be done is either passing in an ObjectTreeParser that has already
    //        parsed the message, or creating an OTP here (which would have the downside that the
    //        password dialog for decrypting messages is shown twice)
#if 0 // totally broken
Laurent Montel's avatar
Laurent Montel committed
375
    KMime::Content *topContent = content->topLevel();
376
    MimeTreeParser::NodeHelper *mNodeHelper = new MimeTreeParser::NodeHelper;
377
    bool bSaveEncrypted = false;
Laurent Montel's avatar
Laurent Montel committed
378
379
380
    bool bEncryptedParts = mNodeHelper->encryptionState(content)
                           != MimeTreeParser::KMMsgNotEncrypted;
    if (bEncryptedParts) {
381
        if (KMessageBox::questionYesNo(parent,
Laurent Montel's avatar
Laurent Montel committed
382
383
384
385
386
387
                                       i18n(
                                           "The part %1 of the message is encrypted. Do you want to keep the encryption when saving?",
                                           url.fileName()),
                                       i18n("KMail Question"), KGuiItem(i18n("Keep Encryption")),
                                       KGuiItem(i18n("Do Not Keep")))
            == KMessageBox::Yes) {
388
389
            bSaveEncrypted = true;
        }
Laurent Montel's avatar
Laurent Montel committed
390
    }
391
392

    bool bSaveWithSig = true;
Laurent Montel's avatar
Laurent Montel committed
393
    if (mNodeHelper->signatureState(content) != MessageViewer::MimeTreeParser::KMMsgNotSigned) {
394
        if (KMessageBox::questionYesNo(parent,
Laurent Montel's avatar
Laurent Montel committed
395
396
397
398
399
400
                                       i18n(
                                           "The part %1 of the message is signed. Do you want to keep the signature when saving?",
                                           url.fileName()),
                                       i18n("KMail Question"), KGuiItem(i18n("Keep Signature")),
                                       KGuiItem(i18n("Do Not Keep")))
            != KMessageBox::Yes) {
401
402
            bSaveWithSig = false;
        }
Laurent Montel's avatar
Laurent Montel committed
403
    }
404
405
406
407
408
409
410

    QByteArray data;
    if (bSaveEncrypted || !bEncryptedParts) {
        KMime::Content *dataNode = content;
        QByteArray rawDecryptedBody;
        bool gotRawDecryptedBody = false;
        if (!bSaveWithSig) {
Laurent Montel's avatar
Laurent Montel committed
411
            if (topContent->contentType()->mimeType() == "multipart/signed") {
412
                // carefully look for the part that is *not* the signature part:
Laurent Montel's avatar
Laurent Montel committed
413
414
415
416
417
418
419
420
421
422
423
424
425
426
                if (MimeTreeParser::ObjectTreeParser::findType(topContent,
                                                               "application/pgp-signature", true,
                                                               false)) {
                    dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
                                                                              "application",
                                                                              "pgp-signature", true,
                                                                              false);
                } else if (MimeTreeParser::ObjectTreeParser::findType(topContent,
                                                                      "application/pkcs7-mime",
                                                                      true, false)) {
                    dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
                                                                              "application",
                                                                              "pkcs7-mime", true,
                                                                              false);
427
                } else {
Laurent Montel's avatar
Laurent Montel committed
428
429
430
                    dataNode = MimeTreeParser::ObjectTreeParser ::findTypeNot(topContent,
                                                                              "multipart", "", true,
                                                                              false);
431
432
433
                }
            } else {
                EmptySource emptySource;
434
                MimeTreeParser::ObjectTreeParser otp(&emptySource, 0, 0, false, false);
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450

                // process this node and all it's siblings and descendants
                mNodeHelper->setNodeUnprocessed(dataNode, true);
                otp.parseObjectTree(dataNode);

                rawDecryptedBody = otp.rawDecryptedBody();
                gotRawDecryptedBody = true;
            }
        }
        QByteArray cstr = gotRawDecryptedBody
                          ? rawDecryptedBody
                          : dataNode->decodedContent();
        data = KMime::CRLFtoLF(cstr);
    }
#else
    const QByteArray data = content->decodedContent();
Laurent Montel's avatar
Laurent Montel committed
451
452
    qCWarning(MESSAGEVIEWER_LOG)
        << "Port the encryption/signature handling when saving a KMime::Content.";
453
454
455
456
457
458
459
460
461
462
#endif
    QDataStream ds;
    QFile file;
    QTemporaryFile tf;
    if (url.isLocalFile()) {
        // save directly
        file.setFileName(url.toLocalFile());
        if (!file.open(QIODevice::WriteOnly)) {
            KMessageBox::error(parent,
                               xi18nc("1 = file name, 2 = error string",
Albert Astals Cid's avatar
Albert Astals Cid committed
463
                                      "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
                                      file.fileName(),
                                      file.errorString()),
                               i18n("Error saving attachment"));
            return false;
        }
        ds.setDevice(&file);
    } else {
        // tmp file for upload
        tf.open();
        ds.setDevice(&tf);
    }

    const int bytesWritten = ds.writeRawData(data.data(), data.size());
    if (bytesWritten != data.size()) {
        QFile *f = static_cast<QFile *>(ds.device());
        KMessageBox::error(parent,
                           xi18nc("1 = file name, 2 = error string",
Albert Astals Cid's avatar
Albert Astals Cid committed
481
                                  "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
                                  f->fileName(),
                                  f->errorString()),
                           i18n("Error saving attachment"));
        // Remove the newly created empty or partial file
        f->remove();
        return false;
    }

    if (!url.isLocalFile()) {
        // QTemporaryFile::fileName() is only defined while the file is open
        QString tfName = tf.fileName();
        tf.close();
        auto job = KIO::file_copy(QUrl::fromLocalFile(tfName), url);
        KJobWidgets::setWindow(job, parent);
        if (!job->exec()) {
            KMessageBox::error(parent,
                               xi18nc("1 = file name, 2 = error string",
Albert Astals Cid's avatar
Albert Astals Cid committed
499
                                      "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
500
501
502
503
504
505
506
507
508
509
510
511
                                      url.toDisplayString(),
                                      job->errorString()),
                               i18n("Error saving attachment"));
            return false;
        }
    } else {
        file.close();
    }

    return true;
}

Laurent Montel's avatar
Laurent Montel committed
512
bool Util::saveAttachments(const KMime::Content::List &contents, QWidget *parent, QList<QUrl> &urlList)
513
514
515
516
517
518
{
    if (contents.isEmpty()) {
        KMessageBox::information(parent, i18n("Found no attachments to save."));
        return false;
    }

Laurent Montel's avatar
Laurent Montel committed
519
    return Util::saveContents(parent, contents, urlList);
520
521
}

522
QString Util::generateFileNameForExtension(const Akonadi::Item &msgBase, const QString &extension)
523
524
525
526
{
    QString fileName;

    if (msgBase.hasPayload<KMime::Message::Ptr>()) {
Laurent Montel's avatar
Laurent Montel committed
527
        fileName
528
            = MessageCore::StringUtil::cleanFileName(MessageCore::StringUtil::cleanSubject(
Laurent Montel's avatar
Laurent Montel committed
529
530
                                                         msgBase.
                                                         payload
Laurent Montel's avatar
Laurent Montel committed
531
                                                         <KMime::Message::Ptr>().data()).trimmed());
532
533
534
535
536
        fileName.remove(QLatin1Char('\"'));
    } else {
        fileName = i18n("message");
    }

537
538
    if (!fileName.endsWith(extension)) {
        fileName += extension;
539
    }
540
541
542
    return fileName;
}

543
544
545
546
547
QString Util::generateMboxFileName(const Akonadi::Item &msgBase)
{
    return Util::generateFileNameForExtension(msgBase, QStringLiteral(".mbox"));
}

548
549
550
551
552
553
554
bool Util::saveMessageInMboxAndGetUrl(QUrl &url, const Akonadi::Item::List &retrievedMsgs, QWidget *parent, bool appendMessages)
{
    if (retrievedMsgs.isEmpty()) {
        return false;
    }
    const Akonadi::Item msgBase = retrievedMsgs.first();
    QString fileName = generateMboxFileName(msgBase);
555
556
557

    const QString filter = i18n("email messages (*.mbox);;all files (*)");

558
    QString fileClass;
Laurent Montel's avatar
Laurent Montel committed
559
560
561
    const QUrl startUrl = KFileWidget::getStartUrl(QUrl(QStringLiteral(
                                                            "kfiledialog:///savemessage")),
                                                   fileClass);
562
563
    QUrl localUrl;
    localUrl.setPath(startUrl.path() + QLatin1Char('/') + fileName);
564
    QFileDialog::Options opt;
Laurent Montel's avatar
Laurent Montel committed
565
    if (appendMessages) {
566
        opt |= QFileDialog::DontConfirmOverwrite;
Laurent Montel's avatar
Laurent Montel committed
567
    }
568
    QUrl dirUrl = QFileDialog::getSaveFileUrl(parent, i18np("Save Message", "Save Messages",
569
                                                            retrievedMsgs.count()), QUrl::fromLocalFile(localUrl.toString()), filter, nullptr,
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
                                              opt);
    if (!dirUrl.isEmpty()) {
        QFile file;
        QTemporaryFile tf;
        QString localFileName;
        if (dirUrl.isLocalFile()) {
            // save directly
            file.setFileName(dirUrl.toLocalFile());
            localFileName = file.fileName();
            if (!appendMessages) {
                QFile::remove(localFileName);
            }
        } else {
            // tmp file for upload
            tf.open();
            localFileName = tf.fileName();
586
587
588
589
590
        }

        KMBox::MBox mbox;
        if (!mbox.load(localFileName)) {
            if (appendMessages) {
Laurent Montel's avatar
Laurent Montel committed
591
592
                KMessageBox::error(parent, i18n("File %1 could not be loaded.",
                                                localFileName), i18n("Error loading message"));
593
            } else {
Laurent Montel's avatar
Laurent Montel committed
594
595
                KMessageBox::error(parent, i18n("File %1 could not be created.",
                                                localFileName), i18n("Error saving message"));
596
597
598
            }
            return false;
        }
Laurent Montel's avatar
Laurent Montel committed
599
        for (const Akonadi::Item &item : qAsConst(retrievedMsgs)) {
600
601
602
603
604
605
            if (item.hasPayload<KMime::Message::Ptr>()) {
                mbox.appendMessage(item.payload<KMime::Message::Ptr>());
            }
        }

        if (!mbox.save()) {
Laurent Montel's avatar
Laurent Montel committed
606
607
            KMessageBox::error(parent, i18n("We cannot save message."),
                               i18n("Error saving message"));
608
609
            return false;
        }
610
        localUrl = QUrl::fromLocalFile(localFileName);
611
612
        if (localUrl.isLocalFile()) {
            KRecentDirs::add(fileClass, localUrl.adjusted(
Laurent Montel's avatar
Laurent Montel committed
613
                                 QUrl::RemoveFilename | QUrl::StripTrailingSlash).path());
614
        }
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633

        if (!dirUrl.isLocalFile()) {
            // QTemporaryFile::fileName() is only defined while the file is open
            QString tfName = tf.fileName();
            tf.close();
            auto job = KIO::file_copy(QUrl::fromLocalFile(tfName), dirUrl);
            KJobWidgets::setWindow(job, parent);
            if (!job->exec()) {
                KMessageBox::error(parent,
                                   xi18nc("1 = file name, 2 = error string",
                                          "<qt>Could not write to the file<br /><filename>%1</filename><br /><br />%2</qt>",
                                          url.toDisplayString(),
                                          job->errorString()),
                                   i18n("Error saving message"));
                return false;
            }
        } else {
            file.close();
        }
634
        url = localUrl;
635
636
637
638
    }
    return true;
}

639
640
641
642
643
644
bool Util::saveMessageInMbox(const Akonadi::Item::List &retrievedMsgs, QWidget *parent, bool appendMessages)
{
    QUrl url;
    return saveMessageInMboxAndGetUrl(url, retrievedMsgs, parent, appendMessages);
}

Laurent Montel's avatar
Laurent Montel committed
645
QAction *Util::createAppAction(const KService::Ptr &service, bool singleOffer, QActionGroup *actionGroup, QObject *parent)
646
647
648
649
650
651
652
653
654
655
656
657
658
659
{
    QString actionName(service->name().replace(QLatin1Char('&'), QStringLiteral("&&")));
    if (singleOffer) {
        actionName = i18n("Open &with %1", actionName);
    } else {
        actionName = i18nc("@item:inmenu Open With, %1 is application name", "%1", actionName);
    }

    QAction *act = new QAction(parent);
    act->setIcon(QIcon::fromTheme(service->icon()));
    act->setText(actionName);
    actionGroup->addAction(act);
    act->setData(QVariant::fromValue(service));
    return act;
660
}
661
662
663
664
665
666
667
668
669

bool Util::excludeExtraHeader(const QString &s)
{
    QRegularExpression ref(QStringLiteral("http-equiv=\\s*(\'|\")(&#82;|R)EFRESH(\'|\")"), QRegularExpression::CaseInsensitiveOption);
    if (s.contains(ref)) {
        return true;
    }
    return false;
}
Laurent Montel's avatar
Laurent Montel committed
670
671
672
673
674
675
676
677
678

void Util::addHelpTextAction(QAction *act, const QString &text)
{
    act->setStatusTip(text);
    act->setToolTip(text);
    if (act->whatsThis().isEmpty()) {
        act->setWhatsThis(text);
    }
}
Laurent Montel's avatar
Laurent Montel committed
679
680
681
682
683
684
685
686
687

const QTextCodec *Util::codecForName(const QByteArray &_str)
{
    if (_str.isEmpty()) {
        return nullptr;
    }
    const QByteArray codec = _str.toLower();
    return KCharsets::charsets()->codecForName(QLatin1String(codec));
}
Laurent Montel's avatar
Laurent Montel committed
688
689
690
691
692
693
694
695
696

void Util::readGravatarConfig()
{
    Gravatar::GravatarCache::self()->setMaximumSize(
        Gravatar::GravatarSettings::self()->gravatarCacheSize());
    if (!Gravatar::GravatarSettings::self()->gravatarSupportEnabled()) {
        Gravatar::GravatarCache::self()->clear();
    }
}
697
698
699
700
701

// FIXME this used to go through the full webkit parser to extract the body and head blocks
// until we have that back, at least attempt to fix some of the damage
// yes, "parsing" HTML with regexps is very very wrong, but it's still better than not filtering
// this at all...
702
Util::HtmlMessageInfo Util::processHtml(const QString &htmlSource)
703
{
704
    Util::HtmlMessageInfo messageInfo;
705
    QString s = htmlSource.trimmed();
Laurent Montel's avatar
Laurent Montel committed
706
    static QRegularExpression docTypeRegularExpression = QRegularExpression(QStringLiteral("<!DOCTYPE[^>]*>"), QRegularExpression::CaseInsensitiveOption);
Laurent Montel's avatar
Laurent Montel committed
707
708
    QRegularExpressionMatch matchDocType;
    const int indexDoctype = s.indexOf(docTypeRegularExpression, 0, &matchDocType);
709
    QString textBeforeDoctype;
Laurent Montel's avatar
Laurent Montel committed
710
    if (indexDoctype > 0) {
711
        textBeforeDoctype = s.left(indexDoctype);
712
        s.remove(textBeforeDoctype);
713
    }
Laurent Montel's avatar
Laurent Montel committed
714
    const QString capturedString = matchDocType.captured();
Laurent Montel's avatar
Laurent Montel committed
715
716
    if (!capturedString.isEmpty()) {
        s = s.remove(capturedString).trimmed();
Laurent Montel's avatar
Laurent Montel committed
717
718
719
    }
    static QRegularExpression htmlRegularExpression = QRegularExpression(QStringLiteral("<html[^>]*>"), QRegularExpression::CaseInsensitiveOption);
    s = s.remove(htmlRegularExpression).trimmed();
720
    // head
Laurent Montel's avatar
Laurent Montel committed
721
722
    static QRegularExpression headEndRegularExpression = QRegularExpression(QStringLiteral("^<head/>"), QRegularExpression::CaseInsensitiveOption);
    s = s.remove(headEndRegularExpression).trimmed();
723
724
725
726
727
    const int startIndex = s.indexOf(QLatin1String("<head>"), Qt::CaseInsensitive);
    if (startIndex >= 0) {
        const auto endIndex = s.indexOf(QLatin1String("</head>"), Qt::CaseInsensitive);

        if (endIndex < 0) {
728
729
            messageInfo.htmlSource = htmlSource;
            return messageInfo;
730
        }
Laurent Montel's avatar
Laurent Montel committed
731
732
        const int index = startIndex + 6;
        messageInfo.extraHead = s.mid(index, endIndex - index);
733
734
735
736
#if QTWEBENGINEWIDGETS_VERSION < QT_VERSION_CHECK(5, 13, 0)
        //Remove this hack with https://codereview.qt-project.org/#/c/256100/2 is merged
        //Don't authorize to refresh content.
        if (MessageViewer::Util::excludeExtraHeader(s)) {
737
            messageInfo.extraHead.clear();
738
739
        }
#endif
740
        s = s.remove(startIndex, endIndex - startIndex + 7).trimmed();
741
742
    }
    // body
Laurent Montel's avatar
Laurent Montel committed
743
    static QRegularExpression body = QRegularExpression(QStringLiteral("<body[^>]*>"), QRegularExpression::CaseInsensitiveOption);
Laurent Montel's avatar
Laurent Montel committed
744
745
746
747
748
749
750
751
    QRegularExpressionMatch matchBody;
    const int bodyStartIndex = s.indexOf(body, 0, &matchBody);
    if (bodyStartIndex >= 0) {
        //qDebug() << "matchBody  " << matchBody.captured();
        s = s.remove(bodyStartIndex, matchBody.capturedLength()).trimmed();
        //Parse style
        messageInfo.bodyStyle = matchBody.captured();
    }
752
    //Some mail has </div>$ at end
Laurent Montel's avatar
Laurent Montel committed
753
754
755
756
757
    static QRegularExpression htmlDivRegularExpression = QRegularExpression(QStringLiteral("(</html></div>|</html>)$"), QRegularExpression::CaseInsensitiveOption);
    s = s.remove(htmlDivRegularExpression).trimmed();
    //s = s.remove(QRegularExpression(QStringLiteral("</html>$"), QRegularExpression::CaseInsensitiveOption)).trimmed();
    static QRegularExpression bodyEndRegularExpression = QRegularExpression(QStringLiteral("</body>$"), QRegularExpression::CaseInsensitiveOption);
    s = s.remove(bodyEndRegularExpression).trimmed();
Laurent Montel's avatar
Laurent Montel committed
758
    messageInfo.htmlSource = textBeforeDoctype + s;
759
    return messageInfo;
760
}
761
762
763
764
765
766
767

QByteArray Util::htmlCodec(const QByteArray &data, const QByteArray &codec)
{
    QByteArray currentCodec = codec;
    if (currentCodec.isEmpty()) {
        currentCodec = QByteArray("UTF-8");
    }
Laurent Montel's avatar
Laurent Montel committed
768
    if (data.contains("charset=\"utf-8\"")
Laurent Montel's avatar
Laurent Montel committed
769
770
        || data.contains("charset=\"UTF-8\"")
        || data.contains("charset=UTF-8")) {
771
772
        currentCodec = QByteArray("UTF-8");
    }
Laurent Montel's avatar
Laurent Montel committed
773
774

    //qDebug() << " codec ******************************************: " << codec << " currentCodec : " <<currentCodec;
775
776
    return currentCodec;
}