File indexing completed on 2024-11-24 04:53:24

0001 /* Copyright (C) 2012 Thomas Lübking <thomas.luebking@gmail.com>
0002    Copyright (C) 2013 Caspar Schutijser <caspar@schutijser.com>
0003    Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
0004    Copyright (C) 2013 - 2014 Pali Rohár <pali.rohar@gmail.com>
0005 
0006    This file is part of the Trojita Qt IMAP e-mail client,
0007    http://trojita.flaska.net/
0008 
0009    This program is free software; you can redistribute it and/or
0010    modify it under the terms of the GNU General Public License as
0011    published by the Free Software Foundation; either version 2 of
0012    the License or (at your option) version 3 or any later version
0013    accepted by the membership of KDE e.V. (or its successor approved
0014    by the membership of KDE e.V.), which shall act as a proxy
0015    defined in Section 14 of version 3 of the license.
0016 
0017    This program is distributed in the hope that it will be useful,
0018    but WITHOUT ANY WARRANTY; without even the implied warranty of
0019    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0020    GNU General Public License for more details.
0021 
0022    You should have received a copy of the GNU General Public License
0023    along with this program.  If not, see <http://www.gnu.org/licenses/>.
0024 */
0025 
0026 #include "AbookAddressbook.h"
0027 #include "be-contacts.h"
0028 
0029 #include <QDir>
0030 #include <QFileSystemWatcher>
0031 #include <QRegularExpression>
0032 #include <QSettings>
0033 #include <QStandardItemModel>
0034 #include <QStringBuilder>
0035 #include <QTimer>
0036 #include "Common/SettingsCategoryGuard.h"
0037 
0038 class AbookAddressbookCompletionJob : public AddressbookCompletionJob
0039 {
0040 public:
0041     AbookAddressbookCompletionJob(const QString &input, const QStringList &ignores, int max, AbookAddressbook *parent) :
0042         AddressbookCompletionJob(parent), m_input(input), m_ignores(ignores), m_max(max), m_parent(parent) {}
0043 
0044 public slots:
0045     virtual void doStart()
0046     {
0047         NameEmailList completion = m_parent->complete(m_input, m_ignores, m_max);
0048         emit completionAvailable(completion);
0049         finished();
0050     }
0051 
0052     virtual void doStop()
0053     {
0054         emit error(AddressbookJob::Stopped);
0055         finished();
0056     }
0057 
0058 private:
0059     QString m_input;
0060     QStringList m_ignores;
0061     int m_max;
0062     AbookAddressbook *m_parent;
0063 
0064 };
0065 
0066 class AbookAddressbookNamesJob : public AddressbookNamesJob
0067 {
0068 public:
0069     AbookAddressbookNamesJob(const QString &email, AbookAddressbook *parent) :
0070         AddressbookNamesJob(parent), m_email(email), m_parent(parent) {}
0071 
0072 public slots:
0073     virtual void doStart()
0074     {
0075         QStringList displayNames = m_parent->prettyNamesForAddress(m_email);
0076         emit prettyNamesForAddressAvailable(displayNames);
0077         finished();
0078     }
0079 
0080     virtual void doStop()
0081     {
0082         emit error(AddressbookJob::Stopped);
0083         finished();
0084     }
0085 
0086 private:
0087     QString m_email;
0088     AbookAddressbook *m_parent;
0089 
0090 };
0091 
0092 AbookAddressbook::AbookAddressbook(QObject *parent): AddressbookPlugin(parent), m_updateTimer(0)
0093 {
0094 #define ADD(TYPE, KEY) \
0095     m_fields << qMakePair<Type,QString>(TYPE, QLatin1String(KEY))
0096     ADD(Name, "name");
0097     ADD(Mail, "email");
0098     ADD(Address, "address");
0099     ADD(City, "city");
0100     ADD(State, "state");
0101     ADD(ZIP, "zip");
0102     ADD(Country, "country");
0103     ADD(Phone, "phone");
0104     ADD(Workphone, "workphone");
0105     ADD(Fax, "fax");
0106     ADD(Mobile, "mobile");
0107     ADD(Nick, "nick");
0108     ADD(URL, "url");
0109     ADD(Notes, "notes");
0110     ADD(Anniversary, "anniversary");
0111     ADD(Photo, "photo");
0112 #undef ADD
0113 
0114     m_contacts = new QStandardItemModel(this);
0115 
0116     ensureAbookPath();
0117 
0118     // read abook
0119     readAbook(false);
0120 
0121     m_filesystemWatcher = new QFileSystemWatcher(this);
0122     m_filesystemWatcher->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook"));
0123     connect (m_filesystemWatcher, &QFileSystemWatcher::fileChanged, this, &AbookAddressbook::scheduleAbookUpdate);
0124 }
0125 
0126 AbookAddressbook::~AbookAddressbook()
0127 {
0128 }
0129 
0130 AddressbookPlugin::Features AbookAddressbook::features() const
0131 {
0132     return FeatureAddressbookWindow | FeatureContactWindow | FeatureAddContact | FeatureEditContact | FeatureCompletion | FeaturePrettyNames;
0133 }
0134 
0135 AddressbookCompletionJob *AbookAddressbook::requestCompletion(const QString &input, const QStringList &ignores, int max)
0136 {
0137     return new AbookAddressbookCompletionJob(input, ignores, max, this);
0138 }
0139 
0140 AddressbookNamesJob *AbookAddressbook::requestPrettyNamesForAddress(const QString &email)
0141 {
0142     return new AbookAddressbookNamesJob(email, this);
0143 }
0144 
0145 void AbookAddressbook::openAddressbookWindow()
0146 {
0147     BE::Contacts *window = new BE::Contacts(this);
0148     window->setAttribute(Qt::WA_DeleteOnClose, true);
0149     //: Translators: BE::Contacts is the name of a stand-alone address book application.
0150     //: BE refers to Bose/Einstein (condensate).
0151     window->setWindowTitle(BE::Contacts::tr("BE::Contacts"));
0152     window->show();
0153 }
0154 
0155 void AbookAddressbook::openContactWindow(const QString &email, const QString &displayName)
0156 {
0157     BE::Contacts *window = new BE::Contacts(this);
0158     window->setAttribute(Qt::WA_DeleteOnClose, true);
0159     window->manageContact(email, displayName);
0160     window->show();
0161 }
0162 
0163 QStandardItemModel *AbookAddressbook::model() const
0164 {
0165     return m_contacts;
0166 }
0167 
0168 void AbookAddressbook::remonitorAdressbook()
0169 {
0170     m_filesystemWatcher->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook"));
0171 }
0172 
0173 void AbookAddressbook::ensureAbookPath()
0174 {
0175     if (!QDir::home().exists(QStringLiteral(".abook"))) {
0176         QDir::home().mkdir(QStringLiteral(".abook"));
0177     }
0178     QDir abook(QDir::homePath() + QLatin1String("/.abook/"));
0179     QStringList abookrc;
0180     QFile file(QDir::homePath() + QLatin1String("/.abook/abookrc"));
0181     if (file.exists() && file.open(QIODevice::ReadWrite|QIODevice::Text)) {
0182         abookrc = QString::fromLocal8Bit(file.readAll()).split(QStringLiteral("\n"));
0183         bool havePhoto = false;
0184         for (QStringList::iterator it = abookrc.begin(), end = abookrc.end(); it != end; ++it) {
0185             if (it->contains(QLatin1String("preserve_fields")))
0186                 *it = QStringLiteral("set preserve_fields=all");
0187             else if (it->contains(QLatin1String("photo")) && it->contains(QLatin1String("field")))
0188                 havePhoto = true;
0189         }
0190         if (!havePhoto)
0191             abookrc << QStringLiteral("field photo = Photo");
0192     } else {
0193         abookrc << QStringLiteral("field photo = Photo") << QStringLiteral("set preserve_fields=all");
0194         file.open(QIODevice::WriteOnly|QIODevice::Text);
0195     }
0196     if (file.isOpen()) {
0197         if (file.isWritable()) {
0198             file.seek(0);
0199             file.write(abookrc.join(QStringLiteral("\n")).toLocal8Bit());
0200         }
0201         file.close();
0202     }
0203     QFile abookFile(abook.filePath(QStringLiteral("addressbook")));
0204     if (!abookFile.exists()) {
0205         abookFile.open(QIODevice::WriteOnly);
0206     }
0207 }
0208 
0209 void AbookAddressbook::scheduleAbookUpdate()
0210 {
0211     // we need to schedule this because the filesystemwatcher usually fires while the file is re/written
0212     if (!m_updateTimer) {
0213         m_updateTimer = new QTimer(this);
0214         m_updateTimer->setSingleShot(true);
0215         connect(m_updateTimer, &QTimer::timeout, this, &AbookAddressbook::updateAbook);
0216     }
0217     m_updateTimer->start(500);
0218 }
0219 
0220 void AbookAddressbook::updateAbook()
0221 {
0222     readAbook(true);
0223     // QFileSystemWatcher will usually unhook from the file when it's re/written - the entire watcher ain't so great :-(
0224     m_filesystemWatcher->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook"));
0225 }
0226 
0227 void AbookAddressbook::readAbook(bool update)
0228 {
0229 //     QElapsedTimer profile;
0230 //     profile.start();
0231     QSettings abook(QDir::homePath() + QLatin1String("/.abook/addressbook"), QSettings::IniFormat);
0232     abook.setIniCodec("UTF-8");
0233     QStringList contacts = abook.childGroups();
0234     foreach (const QString &contact, contacts) {
0235         Common::SettingsCategoryGuard guard(&abook, contact);
0236         QStandardItem *item = 0;
0237         QStringList mails;
0238         if (update) {
0239             QList<QStandardItem*> list = m_contacts->findItems(abook.value(QStringLiteral("name")).toString());
0240             if (list.count() == 1)
0241                 item = list.at(0);
0242             else if (list.count() > 1) {
0243                 mails = abook.value(QStringLiteral("email"), QString()).toStringList();
0244                 const QString mailString = mails.join(QStringLiteral("\n"));
0245                 foreach (QStandardItem *it, list) {
0246                     if (it->data(Mail).toString() == mailString) {
0247                         item = it;
0248                         break;
0249                     }
0250                 }
0251             }
0252             if (item && item->data(Dirty).toBool()) {
0253                 continue;
0254             }
0255         }
0256         bool add = !item;
0257         if (add)
0258             item = new QStandardItem;
0259 
0260         QMap<QString,QVariant> unknownKeys;
0261 
0262         foreach (const QString &key, abook.allKeys()) {
0263             QList<QPair<Type,QString> >::const_iterator field = m_fields.constBegin();
0264             while (field != m_fields.constEnd()) {
0265                 if (field->second == key)
0266                     break;
0267                 ++field;
0268             }
0269             if (field == m_fields.constEnd())
0270                 unknownKeys.insert(key, abook.value(key));
0271             else if (field->first == Mail) {
0272                 if (mails.isEmpty())
0273                     mails = abook.value(field->second, QString()).toStringList(); // to fix the name field
0274                 item->setData( mails.join(QStringLiteral("\n")), Mail );
0275             }
0276             else
0277                 item->setData( abook.value(field->second, QString()), field->first );
0278         }
0279 
0280         // attempt to fix the name field
0281         if (item->data(Name).toString().isEmpty()) {
0282             if (!mails.isEmpty())
0283                 item->setData( mails.at(0), Name );
0284         }
0285         if (item->data(Name).toString().isEmpty()) {
0286             delete item;
0287             continue; // junk or format spec entry
0288         }
0289 
0290         item->setData( unknownKeys, UnknownKeys );
0291 
0292         if (add)
0293             m_contacts->appendRow( item );
0294     }
0295 
0296     m_contacts->sort(0);
0297 //     const qint64 elapsed = profile.elapsed();
0298 //     qDebug() << "reading too" << elapsed << "ms";
0299 }
0300 
0301 void AbookAddressbook::saveContacts()
0302 {
0303     m_filesystemWatcher->blockSignals(true);
0304     QSettings abook(QDir::homePath() + QLatin1String("/.abook/addressbook"), QSettings::IniFormat);
0305     abook.setIniCodec("UTF-8");
0306     abook.clear();
0307     for (int i = 0; i < m_contacts->rowCount(); ++i) {
0308         Common::SettingsCategoryGuard guard(&abook, QString::number(i));
0309         QStandardItem *item = m_contacts->item(i);
0310         for (QList<QPair<Type,QString> >::const_iterator   it = m_fields.constBegin(),
0311                                             end = m_fields.constEnd(); it != end; ++it) {
0312             if (it->first == Mail)
0313                 abook.setValue(QStringLiteral("email"), item->data(Mail).toString().split(QStringLiteral("\n")));
0314             else {
0315                 const QVariant v = item->data(it->first);
0316                 if (!v.toString().isEmpty())
0317                     abook.setValue(it->second, v);
0318             }
0319         }
0320         QMap<QString,QVariant> unknownKeys = item->data( UnknownKeys ).toMap();
0321         for (QMap<QString,QVariant>::const_iterator it = unknownKeys.constBegin(),
0322                                                     end = unknownKeys.constEnd(); it != end; ++it) {
0323             abook.setValue(it.key(), it.value());
0324         }
0325     }
0326     abook.sync();
0327     m_filesystemWatcher->blockSignals(false);
0328 }
0329 
0330 static inline bool ignore(const QString &string, const QStringList &ignores)
0331 {
0332     Q_FOREACH (const QString &ignore, ignores) {
0333         if (ignore.contains(string, Qt::CaseInsensitive))
0334             return true;
0335     }
0336     return false;
0337 }
0338 
0339 NameEmailList AbookAddressbook::complete(const QString &string, const QStringList &ignores, int max) const
0340 {
0341     NameEmailList list;
0342     if (string.isEmpty())
0343         return list;
0344     // In e-mail addresses, dot, dash, _ and @ shall be treated as delimiters
0345     QRegularExpression mailMatch(QStringLiteral("[\\.\\-_@]%1").arg(QRegularExpression::escape(string)),
0346                                  QRegularExpression::CaseInsensitiveOption);
0347     // In human readable names, match on word boundaries
0348     QRegularExpression nameMatch(QStringLiteral("\\b%1").arg(QRegularExpression::escape(string)),
0349                                  QRegularExpression::CaseInsensitiveOption);
0350     // These REs are still not perfect, they won't match on e.g. ".net" or "-project", but screw these I say
0351     for (int i = 0; i < m_contacts->rowCount(); ++i) {
0352         QStandardItem *item = m_contacts->item(i);
0353         QString contactName = item->data(Name).toString();
0354         // several mail addresses per contact are stored newline delimited
0355         QStringList contactMails(item->data(Mail).toString().split(QLatin1Char('\n'), Qt::SkipEmptyParts));
0356         if (contactName.contains(nameMatch)) {
0357             Q_FOREACH (const QString &mail, contactMails) {
0358                 if (ignore(mail, ignores))
0359                     continue;
0360                 list << NameEmail(contactName, mail);
0361                 if (list.count() == max)
0362                     return list;
0363             }
0364             continue;
0365         }
0366         Q_FOREACH (const QString &mail, contactMails) {
0367             if (mail.startsWith(string, Qt::CaseInsensitive) ||
0368                     // don't match on the TLD
0369                     mail.section(QLatin1Char('.'), 0, -2).contains(mailMatch)) {
0370                 if (ignore(mail, ignores))
0371                     continue;
0372                 list << NameEmail(contactName, mail);
0373                 if (list.count() == max)
0374                     return list;
0375             }
0376         }
0377     }
0378     return list;
0379 }
0380 
0381 QStringList AbookAddressbook::prettyNamesForAddress(const QString &mail) const
0382 {
0383     QStringList res;
0384     for (int i = 0; i < m_contacts->rowCount(); ++i) {
0385         QStandardItem *item = m_contacts->item(i);
0386         if (QString::compare(item->data(Mail).toString(), mail, Qt::CaseInsensitive) == 0)
0387             res << item->data(Name).toString();
0388     }
0389     return res;
0390 }
0391 
0392 
0393 QString trojita_plugin_AbookAddressbookPlugin::name() const
0394 {
0395     return QStringLiteral("abookaddressbook");
0396 }
0397 
0398 QString trojita_plugin_AbookAddressbookPlugin::description() const
0399 {
0400     return tr("Addressbook in ~/.abook/");
0401 }
0402 
0403 AddressbookPlugin *trojita_plugin_AbookAddressbookPlugin::create(QObject *parent, QSettings *)
0404 {
0405     return new AbookAddressbook(parent);
0406 }