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"
30 #include <QFileSystemWatcher>
32 #include <QStandardItemModel>
33 #include <QStringBuilder>
35 #include "Common/SettingsCategoryGuard.h"
37 class AbookAddressbookCompletionJob
: public AddressbookCompletionJob
40 AbookAddressbookCompletionJob(const QString
&input
, const QStringList
&ignores
, int max
, AbookAddressbook
*parent
) :
41 AddressbookCompletionJob(parent
), m_input(input
), m_ignores(ignores
), m_max(max
), m_parent(parent
) {}
44 virtual void doStart()
46 NameEmailList completion
= m_parent
->complete(m_input
, m_ignores
, m_max
);
47 emit
completionAvailable(completion
);
53 emit
error(AddressbookJob::Stopped
);
59 QStringList m_ignores
;
61 AbookAddressbook
*m_parent
;
65 class AbookAddressbookNamesJob
: public AddressbookNamesJob
68 AbookAddressbookNamesJob(const QString
&email
, AbookAddressbook
*parent
) :
69 AddressbookNamesJob(parent
), m_email(email
), m_parent(parent
) {}
72 virtual void doStart()
74 QStringList displayNames
= m_parent
->prettyNamesForAddress(m_email
);
75 emit
prettyNamesForAddressAvailable(displayNames
);
81 emit
error(AddressbookJob::Stopped
);
87 AbookAddressbook
*m_parent
;
91 AbookAddressbook::AbookAddressbook(QObject
*parent
): AddressbookPlugin(parent
), m_updateTimer(0)
93 #define ADD(TYPE, KEY) \
94 m_fields << qMakePair<Type,QString>(TYPE, QLatin1String(KEY))
97 ADD(Address
, "address");
101 ADD(Country
, "country");
103 ADD(Workphone
, "workphone");
105 ADD(Mobile
, "mobile");
109 ADD(Anniversary
, "anniversary");
113 m_contacts
= new QStandardItemModel(this);
120 m_filesystemWatcher
= new QFileSystemWatcher(this);
121 m_filesystemWatcher
->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook"));
122 connect (m_filesystemWatcher
, &QFileSystemWatcher::fileChanged
, this, &AbookAddressbook::scheduleAbookUpdate
);
125 AbookAddressbook::~AbookAddressbook()
129 AddressbookPlugin::Features
AbookAddressbook::features() const
131 return FeatureAddressbookWindow
| FeatureContactWindow
| FeatureAddContact
| FeatureEditContact
| FeatureCompletion
| FeaturePrettyNames
;
134 AddressbookCompletionJob
*AbookAddressbook::requestCompletion(const QString
&input
, const QStringList
&ignores
, int max
)
136 return new AbookAddressbookCompletionJob(input
, ignores
, max
, this);
139 AddressbookNamesJob
*AbookAddressbook::requestPrettyNamesForAddress(const QString
&email
)
141 return new AbookAddressbookNamesJob(email
, this);
144 void AbookAddressbook::openAddressbookWindow()
146 BE::Contacts
*window
= new BE::Contacts(this);
147 window
->setAttribute(Qt::WA_DeleteOnClose
, true);
148 //: Translators: BE::Contacts is the name of a stand-alone address book application.
149 //: BE refers to Bose/Einstein (condensate).
150 window
->setWindowTitle(BE::Contacts::tr("BE::Contacts"));
154 void AbookAddressbook::openContactWindow(const QString
&email
, const QString
&displayName
)
156 BE::Contacts
*window
= new BE::Contacts(this);
157 window
->setAttribute(Qt::WA_DeleteOnClose
, true);
158 window
->manageContact(email
, displayName
);
162 QStandardItemModel
*AbookAddressbook::model() const
167 void AbookAddressbook::remonitorAdressbook()
169 m_filesystemWatcher
->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook"));
172 void AbookAddressbook::ensureAbookPath()
174 if (!QDir::home().exists(QStringLiteral(".abook"))) {
175 QDir::home().mkdir(QStringLiteral(".abook"));
177 QDir
abook(QDir::homePath() + QLatin1String("/.abook/"));
179 QFile
file(QDir::homePath() + QLatin1String("/.abook/abookrc"));
180 if (file
.exists() && file
.open(QIODevice::ReadWrite
|QIODevice::Text
)) {
181 abookrc
= QString::fromLocal8Bit(file
.readAll()).split(QStringLiteral("\n"));
182 bool havePhoto
= false;
183 for (QStringList::iterator it
= abookrc
.begin(), end
= abookrc
.end(); it
!= end
; ++it
) {
184 if (it
->contains(QLatin1String("preserve_fields")))
185 *it
= QStringLiteral("set preserve_fields=all");
186 else if (it
->contains(QLatin1String("photo")) && it
->contains(QLatin1String("field")))
190 abookrc
<< QStringLiteral("field photo = Photo");
192 abookrc
<< QStringLiteral("field photo = Photo") << QStringLiteral("set preserve_fields=all");
193 file
.open(QIODevice::WriteOnly
|QIODevice::Text
);
196 if (file
.isWritable()) {
198 file
.write(abookrc
.join(QStringLiteral("\n")).toLocal8Bit());
202 QFile
abookFile(abook
.filePath(QStringLiteral("addressbook")));
203 if (!abookFile
.exists()) {
204 abookFile
.open(QIODevice::WriteOnly
);
208 void AbookAddressbook::scheduleAbookUpdate()
210 // we need to schedule this because the filesystemwatcher usually fires while the file is re/written
211 if (!m_updateTimer
) {
212 m_updateTimer
= new QTimer(this);
213 m_updateTimer
->setSingleShot(true);
214 connect(m_updateTimer
, &QTimer::timeout
, this, &AbookAddressbook::updateAbook
);
216 m_updateTimer
->start(500);
219 void AbookAddressbook::updateAbook()
222 // QFileSystemWatcher will usually unhook from the file when it's re/written - the entire watcher ain't so great :-(
223 m_filesystemWatcher
->addPath(QDir::homePath() + QLatin1String("/.abook/addressbook"));
226 void AbookAddressbook::readAbook(bool update
)
228 // QElapsedTimer profile;
230 QSettings
abook(QDir::homePath() + QLatin1String("/.abook/addressbook"), QSettings::IniFormat
);
231 abook
.setIniCodec("UTF-8");
232 QStringList contacts
= abook
.childGroups();
233 foreach (const QString
&contact
, contacts
) {
234 Common::SettingsCategoryGuard
guard(&abook
, contact
);
235 QStandardItem
*item
= 0;
238 QList
<QStandardItem
*> list
= m_contacts
->findItems(abook
.value(QStringLiteral("name")).toString());
239 if (list
.count() == 1)
241 else if (list
.count() > 1) {
242 mails
= abook
.value(QStringLiteral("email"), QString()).toStringList();
243 const QString mailString
= mails
.join(QStringLiteral("\n"));
244 foreach (QStandardItem
*it
, list
) {
245 if (it
->data(Mail
).toString() == mailString
) {
251 if (item
&& item
->data(Dirty
).toBool()) {
257 item
= new QStandardItem
;
259 QMap
<QString
,QVariant
> unknownKeys
;
261 foreach (const QString
&key
, abook
.allKeys()) {
262 QList
<QPair
<Type
,QString
> >::const_iterator field
= m_fields
.constBegin();
263 while (field
!= m_fields
.constEnd()) {
264 if (field
->second
== key
)
268 if (field
== m_fields
.constEnd())
269 unknownKeys
.insert(key
, abook
.value(key
));
270 else if (field
->first
== Mail
) {
272 mails
= abook
.value(field
->second
, QString()).toStringList(); // to fix the name field
273 item
->setData( mails
.join(QStringLiteral("\n")), Mail
);
276 item
->setData( abook
.value(field
->second
, QString()), field
->first
);
279 // attempt to fix the name field
280 if (item
->data(Name
).toString().isEmpty()) {
281 if (!mails
.isEmpty())
282 item
->setData( mails
.at(0), Name
);
284 if (item
->data(Name
).toString().isEmpty()) {
286 continue; // junk or format spec entry
289 item
->setData( unknownKeys
, UnknownKeys
);
292 m_contacts
->appendRow( item
);
294 // const qint64 elapsed = profile.elapsed();
295 // qDebug() << "reading too" << elapsed << "ms";
298 void AbookAddressbook::saveContacts()
300 m_filesystemWatcher
->blockSignals(true);
301 QSettings
abook(QDir::homePath() + QLatin1String("/.abook/addressbook"), QSettings::IniFormat
);
302 abook
.setIniCodec("UTF-8");
304 for (int i
= 0; i
< m_contacts
->rowCount(); ++i
) {
305 Common::SettingsCategoryGuard
guard(&abook
, QString::number(i
));
306 QStandardItem
*item
= m_contacts
->item(i
);
307 for (QList
<QPair
<Type
,QString
> >::const_iterator it
= m_fields
.constBegin(),
308 end
= m_fields
.constEnd(); it
!= end
; ++it
) {
309 if (it
->first
== Mail
)
310 abook
.setValue(QStringLiteral("email"), item
->data(Mail
).toString().split(QStringLiteral("\n")));
312 const QVariant v
= item
->data(it
->first
);
313 if (!v
.toString().isEmpty())
314 abook
.setValue(it
->second
, v
);
317 QMap
<QString
,QVariant
> unknownKeys
= item
->data( UnknownKeys
).toMap();
318 for (QMap
<QString
,QVariant
>::const_iterator it
= unknownKeys
.constBegin(),
319 end
= unknownKeys
.constEnd(); it
!= end
; ++it
) {
320 abook
.setValue(it
.key(), it
.value());
324 m_filesystemWatcher
->blockSignals(false);
327 static inline bool ignore(const QString
&string
, const QStringList
&ignores
)
329 Q_FOREACH (const QString
&ignore
, ignores
) {
330 if (ignore
.contains(string
, Qt::CaseInsensitive
))
336 NameEmailList
AbookAddressbook::complete(const QString
&string
, const QStringList
&ignores
, int max
) const
339 if (string
.isEmpty())
341 // In e-mail addresses, dot, dash, _ and @ shall be treated as delimiters
342 QRegExp mailMatch
= QRegExp(QString::fromUtf8("[\\.\\-_@]%1").arg(QRegExp::escape(string
)), Qt::CaseInsensitive
);
343 // In human readable names, match on word boundaries
344 QRegExp nameMatch
= QRegExp(QString::fromUtf8("\\b%1").arg(QRegExp::escape(string
)), Qt::CaseInsensitive
);
345 // These REs are still not perfect, they won't match on e.g. ".net" or "-project", but screw these I say
346 for (int i
= 0; i
< m_contacts
->rowCount(); ++i
) {
347 QStandardItem
*item
= m_contacts
->item(i
);
348 QString contactName
= item
->data(Name
).toString();
349 // several mail addresses per contact are stored newline delimited
350 QStringList
contactMails(item
->data(Mail
).toString().split(QLatin1Char('\n'), QString::SkipEmptyParts
));
351 if (contactName
.contains(nameMatch
)) {
352 Q_FOREACH (const QString
&mail
, contactMails
) {
353 if (ignore(mail
, ignores
))
355 list
<< NameEmail(contactName
, mail
);
356 if (list
.count() == max
)
361 Q_FOREACH (const QString
&mail
, contactMails
) {
362 if (mail
.startsWith(string
, Qt::CaseInsensitive
) ||
363 // don't match on the TLD
364 mail
.section(QLatin1Char('.'), 0, -2).contains(mailMatch
)) {
365 if (ignore(mail
, ignores
))
367 list
<< NameEmail(contactName
, mail
);
368 if (list
.count() == max
)
376 QStringList
AbookAddressbook::prettyNamesForAddress(const QString
&mail
) const
379 for (int i
= 0; i
< m_contacts
->rowCount(); ++i
) {
380 QStandardItem
*item
= m_contacts
->item(i
);
381 if (QString::compare(item
->data(Mail
).toString(), mail
, Qt::CaseInsensitive
) == 0)
382 res
<< item
->data(Name
).toString();
388 QString
trojita_plugin_AbookAddressbookPlugin::name() const
390 return QStringLiteral("abookaddressbook");
393 QString
trojita_plugin_AbookAddressbookPlugin::description() const
395 return tr("Addressbook in ~/.abook/");
398 AddressbookPlugin
*trojita_plugin_AbookAddressbookPlugin::create(QObject
*parent
, QSettings
*)
400 return new AbookAddressbook(parent
);