Add credits for Marek
[trojita.git] / src / Plugins / AbookAddressbook / AbookAddressbook.cpp
blob8f2ab67e0f2fa515b7234a9cbc270ee1db6b4385
1 /* Copyright (C) 2012 Thomas Lübking <thomas.luebking@gmail.com>
2 Copyright (C) 2013 Caspar Schutijser <caspar@schutijser.com>
3 Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
4 Copyright (C) 2013 - 2014 Pali Rohár <pali.rohar@gmail.com>
6 This file is part of the Trojita Qt IMAP e-mail client,
7 http://trojita.flaska.net/
9 This program is free software; you can redistribute it and/or
10 modify it under the terms of the GNU General Public License as
11 published by the Free Software Foundation; either version 2 of
12 the License or (at your option) version 3 or any later version
13 accepted by the membership of KDE e.V. (or its successor approved
14 by the membership of KDE e.V.), which shall act as a proxy
15 defined in Section 14 of version 3 of the license.
17 This program is distributed in the hope that it will be useful,
18 but WITHOUT ANY WARRANTY; without even the implied warranty of
19 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 GNU General Public License for more details.
22 You should have received a copy of the GNU General Public License
23 along with this program. If not, see <http://www.gnu.org/licenses/>.
26 #include "AbookAddressbook.h"
27 #include "be-contacts.h"
29 #include <QDir>
30 #include <QFileSystemWatcher>
31 #include <QRegularExpression>
32 #include <QSettings>
33 #include <QStandardItemModel>
34 #include <QStringBuilder>
35 #include <QTimer>
36 #include "Common/SettingsCategoryGuard.h"
38 class AbookAddressbookCompletionJob : public AddressbookCompletionJob
40 public:
41 AbookAddressbookCompletionJob(const QString &input, const QStringList &ignores, int max, AbookAddressbook *parent) :
42 AddressbookCompletionJob(parent), m_input(input), m_ignores(ignores), m_max(max), m_parent(parent) {}
44 public slots:
45 virtual void doStart()
47 NameEmailList completion = m_parent->complete(m_input, m_ignores, m_max);
48 emit completionAvailable(completion);
49 finished();
52 virtual void doStop()
54 emit error(AddressbookJob::Stopped);
55 finished();
58 private:
59 QString m_input;
60 QStringList m_ignores;
61 int m_max;
62 AbookAddressbook *m_parent;
66 class AbookAddressbookNamesJob : public AddressbookNamesJob
68 public:
69 AbookAddressbookNamesJob(const QString &email, AbookAddressbook *parent) :
70 AddressbookNamesJob(parent), m_email(email), m_parent(parent) {}
72 public slots:
73 virtual void doStart()
75 QStringList displayNames = m_parent->prettyNamesForAddress(m_email);
76 emit prettyNamesForAddressAvailable(displayNames);
77 finished();
80 virtual void doStop()
82 emit error(AddressbookJob::Stopped);
83 finished();
86 private:
87 QString m_email;
88 AbookAddressbook *m_parent;
92 AbookAddressbook::AbookAddressbook(QObject *parent): AddressbookPlugin(parent), m_updateTimer(0)
94 #define ADD(TYPE, KEY) \
95 m_fields << qMakePair<Type,QString>(TYPE, QLatin1String(KEY))
96 ADD(Name, "name");
97 ADD(Mail, "email");
98 ADD(Address, "address");
99 ADD(City, "city");
100 ADD(State, "state");
101 ADD(ZIP, "zip");
102 ADD(Country, "country");
103 ADD(Phone, "phone");
104 ADD(Workphone, "workphone");
105 ADD(Fax, "fax");
106 ADD(Mobile, "mobile");
107 ADD(Nick, "nick");
108 ADD(URL, "url");
109 ADD(Notes, "notes");
110 ADD(Anniversary, "anniversary");
111 ADD(Photo, "photo");
112 #undef ADD
114 m_contacts = new QStandardItemModel(this);
116 ensureAbookPath();
118 // read abook
119 readAbook(false);
121 m_filesystemWatcher = new QFileSystemWatcher(this);
122 m_filesystemWatcher->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook"));
123 connect (m_filesystemWatcher, &QFileSystemWatcher::fileChanged, this, &AbookAddressbook::scheduleAbookUpdate);
126 AbookAddressbook::~AbookAddressbook()
130 AddressbookPlugin::Features AbookAddressbook::features() const
132 return FeatureAddressbookWindow | FeatureContactWindow | FeatureAddContact | FeatureEditContact | FeatureCompletion | FeaturePrettyNames;
135 AddressbookCompletionJob *AbookAddressbook::requestCompletion(const QString &input, const QStringList &ignores, int max)
137 return new AbookAddressbookCompletionJob(input, ignores, max, this);
140 AddressbookNamesJob *AbookAddressbook::requestPrettyNamesForAddress(const QString &email)
142 return new AbookAddressbookNamesJob(email, this);
145 void AbookAddressbook::openAddressbookWindow()
147 BE::Contacts *window = new BE::Contacts(this);
148 window->setAttribute(Qt::WA_DeleteOnClose, true);
149 //: Translators: BE::Contacts is the name of a stand-alone address book application.
150 //: BE refers to Bose/Einstein (condensate).
151 window->setWindowTitle(BE::Contacts::tr("BE::Contacts"));
152 window->show();
155 void AbookAddressbook::openContactWindow(const QString &email, const QString &displayName)
157 BE::Contacts *window = new BE::Contacts(this);
158 window->setAttribute(Qt::WA_DeleteOnClose, true);
159 window->manageContact(email, displayName);
160 window->show();
163 QStandardItemModel *AbookAddressbook::model() const
165 return m_contacts;
168 void AbookAddressbook::remonitorAdressbook()
170 m_filesystemWatcher->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook"));
173 void AbookAddressbook::ensureAbookPath()
175 if (!QDir::home().exists(QStringLiteral(".abook"))) {
176 QDir::home().mkdir(QStringLiteral(".abook"));
178 QDir abook(QDir::homePath() + QLatin1String("/.abook/"));
179 QStringList abookrc;
180 QFile file(QDir::homePath() + QLatin1String("/.abook/abookrc"));
181 if (file.exists() && file.open(QIODevice::ReadWrite|QIODevice::Text)) {
182 abookrc = QString::fromLocal8Bit(file.readAll()).split(QStringLiteral("\n"));
183 bool havePhoto = false;
184 for (QStringList::iterator it = abookrc.begin(), end = abookrc.end(); it != end; ++it) {
185 if (it->contains(QLatin1String("preserve_fields")))
186 *it = QStringLiteral("set preserve_fields=all");
187 else if (it->contains(QLatin1String("photo")) && it->contains(QLatin1String("field")))
188 havePhoto = true;
190 if (!havePhoto)
191 abookrc << QStringLiteral("field photo = Photo");
192 } else {
193 abookrc << QStringLiteral("field photo = Photo") << QStringLiteral("set preserve_fields=all");
194 file.open(QIODevice::WriteOnly|QIODevice::Text);
196 if (file.isOpen()) {
197 if (file.isWritable()) {
198 file.seek(0);
199 file.write(abookrc.join(QStringLiteral("\n")).toLocal8Bit());
201 file.close();
203 QFile abookFile(abook.filePath(QStringLiteral("addressbook")));
204 if (!abookFile.exists()) {
205 abookFile.open(QIODevice::WriteOnly);
209 void AbookAddressbook::scheduleAbookUpdate()
211 // we need to schedule this because the filesystemwatcher usually fires while the file is re/written
212 if (!m_updateTimer) {
213 m_updateTimer = new QTimer(this);
214 m_updateTimer->setSingleShot(true);
215 connect(m_updateTimer, &QTimer::timeout, this, &AbookAddressbook::updateAbook);
217 m_updateTimer->start(500);
220 void AbookAddressbook::updateAbook()
222 readAbook(true);
223 // QFileSystemWatcher will usually unhook from the file when it's re/written - the entire watcher ain't so great :-(
224 m_filesystemWatcher->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook"));
227 void AbookAddressbook::readAbook(bool update)
229 // QElapsedTimer profile;
230 // profile.start();
231 QSettings abook(QDir::homePath() + QLatin1String("/.abook/addressbook"), QSettings::IniFormat);
232 abook.setIniCodec("UTF-8");
233 QStringList contacts = abook.childGroups();
234 foreach (const QString &contact, contacts) {
235 Common::SettingsCategoryGuard guard(&abook, contact);
236 QStandardItem *item = 0;
237 QStringList mails;
238 if (update) {
239 QList<QStandardItem*> list = m_contacts->findItems(abook.value(QStringLiteral("name")).toString());
240 if (list.count() == 1)
241 item = list.at(0);
242 else if (list.count() > 1) {
243 mails = abook.value(QStringLiteral("email"), QString()).toStringList();
244 const QString mailString = mails.join(QStringLiteral("\n"));
245 foreach (QStandardItem *it, list) {
246 if (it->data(Mail).toString() == mailString) {
247 item = it;
248 break;
252 if (item && item->data(Dirty).toBool()) {
253 continue;
256 bool add = !item;
257 if (add)
258 item = new QStandardItem;
260 QMap<QString,QVariant> unknownKeys;
262 foreach (const QString &key, abook.allKeys()) {
263 QList<QPair<Type,QString> >::const_iterator field = m_fields.constBegin();
264 while (field != m_fields.constEnd()) {
265 if (field->second == key)
266 break;
267 ++field;
269 if (field == m_fields.constEnd())
270 unknownKeys.insert(key, abook.value(key));
271 else if (field->first == Mail) {
272 if (mails.isEmpty())
273 mails = abook.value(field->second, QString()).toStringList(); // to fix the name field
274 item->setData( mails.join(QStringLiteral("\n")), Mail );
276 else
277 item->setData( abook.value(field->second, QString()), field->first );
280 // attempt to fix the name field
281 if (item->data(Name).toString().isEmpty()) {
282 if (!mails.isEmpty())
283 item->setData( mails.at(0), Name );
285 if (item->data(Name).toString().isEmpty()) {
286 delete item;
287 continue; // junk or format spec entry
290 item->setData( unknownKeys, UnknownKeys );
292 if (add)
293 m_contacts->appendRow( item );
296 m_contacts->sort(0);
297 // const qint64 elapsed = profile.elapsed();
298 // qDebug() << "reading too" << elapsed << "ms";
301 void AbookAddressbook::saveContacts()
303 m_filesystemWatcher->blockSignals(true);
304 QSettings abook(QDir::homePath() + QLatin1String("/.abook/addressbook"), QSettings::IniFormat);
305 abook.setIniCodec("UTF-8");
306 abook.clear();
307 for (int i = 0; i < m_contacts->rowCount(); ++i) {
308 Common::SettingsCategoryGuard guard(&abook, QString::number(i));
309 QStandardItem *item = m_contacts->item(i);
310 for (QList<QPair<Type,QString> >::const_iterator it = m_fields.constBegin(),
311 end = m_fields.constEnd(); it != end; ++it) {
312 if (it->first == Mail)
313 abook.setValue(QStringLiteral("email"), item->data(Mail).toString().split(QStringLiteral("\n")));
314 else {
315 const QVariant v = item->data(it->first);
316 if (!v.toString().isEmpty())
317 abook.setValue(it->second, v);
320 QMap<QString,QVariant> unknownKeys = item->data( UnknownKeys ).toMap();
321 for (QMap<QString,QVariant>::const_iterator it = unknownKeys.constBegin(),
322 end = unknownKeys.constEnd(); it != end; ++it) {
323 abook.setValue(it.key(), it.value());
326 abook.sync();
327 m_filesystemWatcher->blockSignals(false);
330 static inline bool ignore(const QString &string, const QStringList &ignores)
332 Q_FOREACH (const QString &ignore, ignores) {
333 if (ignore.contains(string, Qt::CaseInsensitive))
334 return true;
336 return false;
339 NameEmailList AbookAddressbook::complete(const QString &string, const QStringList &ignores, int max) const
341 NameEmailList list;
342 if (string.isEmpty())
343 return list;
344 // In e-mail addresses, dot, dash, _ and @ shall be treated as delimiters
345 QRegularExpression mailMatch(QStringLiteral("[\\.\\-_@]%1").arg(QRegularExpression::escape(string)),
346 QRegularExpression::CaseInsensitiveOption);
347 // In human readable names, match on word boundaries
348 QRegularExpression nameMatch(QStringLiteral("\\b%1").arg(QRegularExpression::escape(string)),
349 QRegularExpression::CaseInsensitiveOption);
350 // These REs are still not perfect, they won't match on e.g. ".net" or "-project", but screw these I say
351 for (int i = 0; i < m_contacts->rowCount(); ++i) {
352 QStandardItem *item = m_contacts->item(i);
353 QString contactName = item->data(Name).toString();
354 // several mail addresses per contact are stored newline delimited
355 QStringList contactMails(item->data(Mail).toString().split(QLatin1Char('\n'), QString::SkipEmptyParts));
356 if (contactName.contains(nameMatch)) {
357 Q_FOREACH (const QString &mail, contactMails) {
358 if (ignore(mail, ignores))
359 continue;
360 list << NameEmail(contactName, mail);
361 if (list.count() == max)
362 return list;
364 continue;
366 Q_FOREACH (const QString &mail, contactMails) {
367 if (mail.startsWith(string, Qt::CaseInsensitive) ||
368 // don't match on the TLD
369 mail.section(QLatin1Char('.'), 0, -2).contains(mailMatch)) {
370 if (ignore(mail, ignores))
371 continue;
372 list << NameEmail(contactName, mail);
373 if (list.count() == max)
374 return list;
378 return list;
381 QStringList AbookAddressbook::prettyNamesForAddress(const QString &mail) const
383 QStringList res;
384 for (int i = 0; i < m_contacts->rowCount(); ++i) {
385 QStandardItem *item = m_contacts->item(i);
386 if (QString::compare(item->data(Mail).toString(), mail, Qt::CaseInsensitive) == 0)
387 res << item->data(Name).toString();
389 return res;
393 QString trojita_plugin_AbookAddressbookPlugin::name() const
395 return QStringLiteral("abookaddressbook");
398 QString trojita_plugin_AbookAddressbookPlugin::description() const
400 return tr("Addressbook in ~/.abook/");
403 AddressbookPlugin *trojita_plugin_AbookAddressbookPlugin::create(QObject *parent, QSettings *)
405 return new AbookAddressbook(parent);