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>
31 #include <QRegularExpression>
33 #include <QStandardItemModel>
34 #include <QStringBuilder>
36 #include "Common/SettingsCategoryGuard.h"
38 class AbookAddressbookCompletionJob
: public AddressbookCompletionJob
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
) {}
45 virtual void doStart()
47 NameEmailList completion
= m_parent
->complete(m_input
, m_ignores
, m_max
);
48 emit
completionAvailable(completion
);
54 emit
error(AddressbookJob::Stopped
);
60 QStringList m_ignores
;
62 AbookAddressbook
*m_parent
;
66 class AbookAddressbookNamesJob
: public AddressbookNamesJob
69 AbookAddressbookNamesJob(const QString
&email
, AbookAddressbook
*parent
) :
70 AddressbookNamesJob(parent
), m_email(email
), m_parent(parent
) {}
73 virtual void doStart()
75 QStringList displayNames
= m_parent
->prettyNamesForAddress(m_email
);
76 emit
prettyNamesForAddressAvailable(displayNames
);
82 emit
error(AddressbookJob::Stopped
);
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))
98 ADD(Address
, "address");
102 ADD(Country
, "country");
104 ADD(Workphone
, "workphone");
106 ADD(Mobile
, "mobile");
110 ADD(Anniversary
, "anniversary");
114 m_contacts
= new QStandardItemModel(this);
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"));
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
);
163 QStandardItemModel
*AbookAddressbook::model() const
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/"));
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")))
191 abookrc
<< QStringLiteral("field photo = Photo");
193 abookrc
<< QStringLiteral("field photo = Photo") << QStringLiteral("set preserve_fields=all");
194 file
.open(QIODevice::WriteOnly
|QIODevice::Text
);
197 if (file
.isWritable()) {
199 file
.write(abookrc
.join(QStringLiteral("\n")).toLocal8Bit());
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()
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;
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;
239 QList
<QStandardItem
*> list
= m_contacts
->findItems(abook
.value(QStringLiteral("name")).toString());
240 if (list
.count() == 1)
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
) {
252 if (item
&& item
->data(Dirty
).toBool()) {
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
)
269 if (field
== m_fields
.constEnd())
270 unknownKeys
.insert(key
, abook
.value(key
));
271 else if (field
->first
== Mail
) {
273 mails
= abook
.value(field
->second
, QString()).toStringList(); // to fix the name field
274 item
->setData( mails
.join(QStringLiteral("\n")), Mail
);
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()) {
287 continue; // junk or format spec entry
290 item
->setData( unknownKeys
, UnknownKeys
);
293 m_contacts
->appendRow( item
);
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");
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")));
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());
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
))
339 NameEmailList
AbookAddressbook::complete(const QString
&string
, const QStringList
&ignores
, int max
) const
342 if (string
.isEmpty())
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
))
360 list
<< NameEmail(contactName
, mail
);
361 if (list
.count() == max
)
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
))
372 list
<< NameEmail(contactName
, mail
);
373 if (list
.count() == max
)
381 QStringList
AbookAddressbook::prettyNamesForAddress(const QString
&mail
) const
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();
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
);