File indexing completed on 2025-01-19 04:46:51

0001 /*
0002 
0003   This file is part of KMail, the KDE mail client.
0004   SPDX-FileCopyrightText: 2004 Till Adam <adam@kde.org>
0005   SPDX-FileCopyrightText: 2010 Klarälvdalens Datakonsult AB, a KDAB Group company <info@kdab.net>
0006   SPDX-FileCopyrightText: 2012-2024 Laurent Montel <montel@kde.org>
0007 
0008   SPDX-License-Identifier: GPL-2.0-or-later
0009 */
0010 
0011 #include "updatecontactjob.h"
0012 #include "vcard_debug.h"
0013 #include "vcardmemento.h"
0014 
0015 #include <KContacts/Addressee>
0016 #include <KContacts/VCardConverter>
0017 
0018 #include <MessageViewer/BodyPartURLHandler>
0019 #include <MessageViewer/MessagePartRenderPlugin>
0020 #include <MessageViewer/MessagePartRendererBase>
0021 
0022 #include <MessageViewer/HtmlWriter>
0023 #include <MimeTreeParser/BodyPart>
0024 #include <MimeTreeParser/MessagePart>
0025 #include <MimeTreeParser/NodeHelper>
0026 using MimeTreeParser::Interface::BodyPart;
0027 
0028 #include <Akonadi/AddContactJob>
0029 
0030 #include <Akonadi/ContactViewer>
0031 #include <Akonadi/StandardContactFormatter>
0032 
0033 #include <KIO/FileCopyJob>
0034 #include <KIO/StatJob>
0035 #include <KIconLoader>
0036 #include <KLocalizedString>
0037 
0038 #include <QFileDialog>
0039 #include <QIcon>
0040 #include <QMenu>
0041 #include <QMimeDatabase>
0042 #include <QTemporaryFile>
0043 
0044 namespace
0045 {
0046 class Formatter : public MessageViewer::MessagePartRendererBase
0047 {
0048 public:
0049     Formatter() = default;
0050 
0051     bool render(const MimeTreeParser::MessagePartPtr &msgPart, MessageViewer::HtmlWriter *writer, MessageViewer::RenderContext *) const override
0052     {
0053         QMimeDatabase db;
0054         auto mt = db.mimeTypeForName(QString::fromLatin1(msgPart->content()->contentType()->mimeType().toLower()));
0055         if (!mt.isValid() || mt.name() != QLatin1StringView("text/vcard")) {
0056             return false;
0057         }
0058 
0059         const QString vCard = msgPart->text();
0060         if (vCard.isEmpty()) {
0061             return false;
0062         }
0063 
0064         KContacts::VCardConverter vcc;
0065 
0066         auto memento = dynamic_cast<MessageViewer::VcardMemento *>(msgPart->memento());
0067         QStringList lst;
0068 
0069         // Pre-count the number of non-empty addressees
0070         int count = 0;
0071         const KContacts::Addressee::List al = vcc.parseVCards(vCard.toUtf8());
0072         for (const KContacts::Addressee &a : al) {
0073             if (a.isEmpty()) {
0074                 continue;
0075             }
0076             if (!memento) {
0077                 if (!a.emails().isEmpty()) {
0078                     lst.append(a.emails().constFirst());
0079                     count++;
0080                 }
0081             }
0082         }
0083         if (!count && !memento) {
0084             return false;
0085         }
0086 
0087         writer->write(QStringLiteral("<div align=\"center\"><h2>") + i18np("Attached business card", "Attached business cards", count)
0088                       + QStringLiteral("</h2></div>"));
0089 
0090         count = 0;
0091         static QString defaultPixmapPath = QUrl::fromLocalFile(KIconLoader::global()->iconPath(QStringLiteral("user-identity"), KIconLoader::Desktop)).url();
0092         static QString defaultMapIconPath = QUrl::fromLocalFile(KIconLoader::global()->iconPath(QStringLiteral("map-symbolic"), KIconLoader::Small)).url();
0093         static QString defaultSmsIconPath = QUrl::fromLocalFile(KIconLoader::global()->iconPath(QStringLiteral("message-new"), KIconLoader::Small)).url();
0094 
0095         if (!memento) {
0096             memento = new MessageViewer::VcardMemento(lst);
0097             msgPart->setMemento(memento);
0098 
0099             auto nodeHelper = msgPart->nodeHelper();
0100             if (nodeHelper) {
0101                 QObject::connect(memento, &MessageViewer::VcardMemento::update, nodeHelper, &MimeTreeParser::NodeHelper::update);
0102             }
0103         }
0104 
0105         for (const KContacts::Addressee &a : al) {
0106             if (a.isEmpty()) {
0107                 continue;
0108             }
0109             Akonadi::StandardContactFormatter formatter;
0110             formatter.setContact(a);
0111             formatter.setDisplayQRCode(false);
0112             QString htmlStr = formatter.toHtml(Akonadi::StandardContactFormatter::EmbeddableForm);
0113             const KContacts::Picture photo = a.photo();
0114             htmlStr.replace(QStringLiteral("<img src=\"map_icon\""), QStringLiteral("<img src=\"%1\" width=\"16\" height=\"16\"").arg(defaultMapIconPath));
0115             htmlStr.replace(QStringLiteral("<img src=\"sms_icon\""), QStringLiteral("<img src=\"%1\" width=\"16\" height=\"16\"").arg(defaultSmsIconPath));
0116             if (photo.isEmpty()) {
0117                 htmlStr.replace(QStringLiteral("img src=\"contact_photo\""), QStringLiteral("img src=\"%1\"").arg(defaultPixmapPath));
0118             } else {
0119                 QImage img = a.photo().data();
0120                 const QString dir = msgPart->nodeHelper()->createTempDir(QLatin1StringView("vcard-") + a.uid());
0121                 const QString filename = dir + QLatin1Char('/') + a.uid();
0122                 img.save(filename, "PNG");
0123                 msgPart->nodeHelper()->addTempFile(filename);
0124                 const QString href = QLatin1StringView("file:") + QLatin1StringView(QUrl::toPercentEncoding(filename));
0125                 htmlStr.replace(QLatin1StringView("img src=\"contact_photo\""), QStringLiteral("img src=\"%1\"").arg(href));
0126             }
0127             writer->write(htmlStr);
0128 
0129             if (!memento || (memento && !memento->finished()) || (memento && memento->finished() && !memento->vcardExist(count))) {
0130                 const QString addToLinkText = i18n("[Add this contact to the address book]");
0131                 QString op = QStringLiteral("addToAddressBook:%1").arg(count);
0132                 writer->write(QStringLiteral("<div align=\"center\"><a href=\"") + msgPart->makeLink(op) + QStringLiteral("\">") + addToLinkText
0133                               + QStringLiteral("</a></div><br/><br/>"));
0134             } else {
0135                 if (memento->address(count) != a) {
0136                     const QString addToLinkText = i18n("[Update this contact in the address book]");
0137                     const QString op = QStringLiteral("updateToAddressBook:%1").arg(count);
0138                     writer->write(QStringLiteral("<div align=\"center\"><a href=\"") + msgPart->makeLink(op) + QStringLiteral("\">") + addToLinkText
0139                                   + QStringLiteral("</a></div><br><br>"));
0140                 } else {
0141                     const QString addToLinkText = i18n("[This contact is already in addressbook]");
0142                     writer->write(QStringLiteral("<div align=\"center\">") + addToLinkText + QStringLiteral("</a></div><br><br>"));
0143                 }
0144             }
0145             count++;
0146         }
0147 
0148         return true;
0149     }
0150 };
0151 
0152 class UrlHandler : public MessageViewer::Interface::BodyPartURLHandler
0153 {
0154 public:
0155     [[nodiscard]] QString name() const override
0156     {
0157         return QStringLiteral("vcardhandler");
0158     }
0159 
0160     bool handleClick(MessageViewer::Viewer *viewerInstance, BodyPart *bodyPart, const QString &path) const override
0161     {
0162         Q_UNUSED(viewerInstance)
0163         const QString vCard = bodyPart->content()->decodedText();
0164         if (vCard.isEmpty()) {
0165             return true;
0166         }
0167         KContacts::VCardConverter vcc;
0168         const KContacts::Addressee::List al = vcc.parseVCards(vCard.toUtf8());
0169         const int index = QStringView(path).right(path.length() - path.lastIndexOf(QLatin1Char(':')) - 1).toInt();
0170         if (index == -1 || index >= al.count()) {
0171             return true;
0172         }
0173         const KContacts::Addressee a = al.at(index);
0174         if (a.isEmpty()) {
0175             return true;
0176         }
0177 
0178         if (path.startsWith(QLatin1StringView("addToAddressBook"))) {
0179             auto job = new Akonadi::AddContactJob(a, nullptr);
0180             job->start();
0181         } else if (path.startsWith(QLatin1StringView("updateToAddressBook"))) {
0182             auto job = new UpdateContactJob(a.emails().constFirst(), a, nullptr);
0183             job->start();
0184         }
0185 
0186         return true;
0187     }
0188 
0189     static KContacts::Addressee findAddressee(BodyPart *part, const QString &path)
0190     {
0191         const QString vCard = part->content()->decodedText();
0192         if (!vCard.isEmpty()) {
0193             KContacts::VCardConverter vcc;
0194             const KContacts::Addressee::List al = vcc.parseVCards(vCard.toUtf8());
0195             const int index = QStringView(path).right(path.length() - path.lastIndexOf(QLatin1Char(':')) - 1).toInt();
0196             if (index >= 0 && index < al.count()) {
0197                 return al.at(index);
0198             }
0199         }
0200         return {};
0201     }
0202 
0203     bool handleContextMenuRequest(BodyPart *part, const QString &path, const QPoint &point) const override
0204     {
0205         const QString vCard = part->content()->decodedText();
0206         if (vCard.isEmpty()) {
0207             return true;
0208         }
0209         KContacts::Addressee a = findAddressee(part, path);
0210         if (a.isEmpty()) {
0211             return true;
0212         }
0213 
0214         auto menu = new QMenu();
0215         QAction *open = menu->addAction(QIcon::fromTheme(QStringLiteral("document-open")), i18n("View Business Card"));
0216         QAction *saveas = menu->addAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save Business Card As..."));
0217 
0218         QAction *action = menu->exec(point, nullptr);
0219         if (action == open) {
0220             openVCard(a, vCard);
0221         } else if (action == saveas) {
0222             saveAsVCard(a, vCard);
0223         }
0224         delete menu;
0225         return true;
0226     }
0227 
0228     QString statusBarMessage(BodyPart *part, const QString &path) const override
0229     {
0230         KContacts::Addressee a = findAddressee(part, path);
0231         const bool addToAddressBook = path.startsWith(QLatin1StringView("addToAddressBook"));
0232         if (a.realName().isEmpty()) {
0233             return addToAddressBook ? i18n("Add this contact to the address book.") : i18n("Update this contact to the address book.");
0234         } else {
0235             return addToAddressBook ? i18n("Add \"%1\" to the address book.", a.realName()) : i18n("Update \"%1\" to the address book.", a.realName());
0236         }
0237     }
0238 
0239     [[nodiscard]] bool openVCard(const KContacts::Addressee &a, const QString &vCard) const
0240     {
0241         Q_UNUSED(vCard)
0242         auto view = new Akonadi::ContactViewer(nullptr);
0243         view->setRawContact(a);
0244         view->setMinimumSize(300, 400);
0245         view->show();
0246         return true;
0247     }
0248 
0249     [[nodiscard]] bool saveAsVCard(const KContacts::Addressee &a, const QString &vCard) const
0250     {
0251         QString fileName;
0252         const QString givenName(a.givenName());
0253         if (givenName.isEmpty()) {
0254             fileName = a.familyName() + QStringLiteral(".vcf");
0255         } else {
0256             fileName = givenName + QLatin1Char('_') + a.familyName() + QStringLiteral(".vcf");
0257         }
0258         // get the saveas file name
0259         QUrl saveAsUrl = QFileDialog::getSaveFileUrl(nullptr, i18n("Save Business Card"), QUrl::fromUserInput(fileName));
0260         if (saveAsUrl.isEmpty()) {
0261             return false;
0262         }
0263 
0264         // put the attachment in a temporary file and save it
0265         QTemporaryFile tmpFile;
0266         tmpFile.open();
0267 
0268         QByteArray data = vCard.toUtf8();
0269         tmpFile.write(data);
0270         tmpFile.flush();
0271         auto job = KIO::file_copy(QUrl::fromLocalFile(tmpFile.fileName()), saveAsUrl, -1, KIO::Overwrite);
0272         return job->exec();
0273     }
0274 };
0275 
0276 class Plugin : public QObject, public MessageViewer::MessagePartRenderPlugin
0277 {
0278     Q_OBJECT
0279     Q_INTERFACES(MessageViewer::MessagePartRenderPlugin)
0280     Q_PLUGIN_METADATA(IID "com.kde.messageviewer.bodypartformatter" FILE "text_vcard.json")
0281 public:
0282     MessageViewer::MessagePartRendererBase *renderer(int index) override
0283     {
0284         return validIndex(index) ? new Formatter() : nullptr;
0285     }
0286 
0287     [[nodiscard]] const MessageViewer::Interface::BodyPartURLHandler *urlHandler(int idx) const override
0288     {
0289         return validIndex(idx) ? new UrlHandler() : nullptr;
0290     }
0291 
0292 private:
0293     [[nodiscard]] bool validIndex(int idx) const
0294     {
0295         return idx == 0;
0296     }
0297 };
0298 }
0299 
0300 #include "text_vcard.moc"