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"