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 }