pyCardDAV-0.6.1/0000775000175000017500000000000012245117351012302 5ustar cgcg00000000000000pyCardDAV-0.6.1/README.rst0000664000175000017500000001742012245116621013774 0ustar cgcg00000000000000About ===== *pyCardDAV* is a simple to use CardDAV_ CLI client. It has built in support for mutt's *query_command* but also works very well solo. *pyCardDAV* consists of *pycardsyncer*, a program for syncing your CardDAV resource into a local database and of *pc_query*, a program for querying the local database. *pyCardDAV* is some ugly python_ code (actually, it's not *that* bad anymore…) that holds together vobject_, lxml_, requests_ and pysqlite_. .. _CardDAV: http://en.wikipedia.org/wiki/CardDAV .. _python: http://python.org/ .. _vobject: http://vobject.skyhouseconsulting.com/ .. _lxml: http://lxml.de/ .. _pysqlite: http://code.google.com/p/pysqlite/ .. _requests: http://python-requests.org Features -------- (or rather: limitations) - *pyCardDAV* can only use one address book resource at the moment - *pyCardDAV* is only tested against davical, owncloud and sabredav - *pyCardDAV* can import the sender's address directly from mutt - *pyCardDAV* can backup and import to/from .vcf files - *pyCardDAV* can add email addresses directly from mutt - *pyCardDAV* only understands VCard 3.0 - *pyCardDAV* is not python 3 compatible yet Feedback -------- Please do provide feedback if *pyCardDAV* works for you or even more importantly if it doesn't. You can reach me by email at pycarddav (at) lostpackets (dot) de , by jabber/XMPP at geier (at) jabber (dot) ccc (dot) de or via github_ .. _github: https://github.com/geier/pycarddav/ Installation ------------ You can download *pyCardDAV* either from the above download link or check it out from git (at github). Then install *pyCardDAV* by executing *python setup.py install*. If you feel more adventurous you can always the *develop* branch on github, which *should* always be in a usable state. pyCardDAV is also available on pypi_ and can be installed via pip install pycarddav or easy_install pycarddav. Copy and edit the supplied pycard.conf.sample file (default location is ~/.config/pycard/pycard.conf). If you don't want to store the password in clear text in the config file, *pyCardDAV* will ask for it while syncing. Make sure you have sqlite3 (normally available by default), vobject, lxml(>2), requests (>0.10), urwid (>0.9) pyxdg, installed. Users of python 2.6 will also need to install argparse. *pyCardDAV* has so far been successfully tested on recent versions of FreeBSD, NetBSD, Debian and Ubuntu with python 2.6 and 2.7 and against davical 0.9.9.4 - 1.0.1 (later versions should be ok, too, but 0.9.9.3 and earlier don't seem to work), owncloud and sabredav. .. _pypi: https://pypi.python.org/pypi/pyCardDAV/ .. _git: http://github.com/geier/pycarddav/ Usage ----- *pyCardDAV* consists of three scripts, *pycardsyncer* which is used to sync the local database with the server, *pc_query* to interact with the local database and *pycard-import* to import email addresses from mutt. Execute pycardsyncer to sync your addresses to the local database. You can test pc_query with:: % pc_query searchstring By default *pyCardDAV* only prints the names, email addresses and telephone numbers of contacts matching the search string, to see all vCard properties use the "-A" option. For usage with mutt etc., *pyCardDAV* can also print only email addresses in a mutt friendly format (with the "-m" option). Edit your mutt configuration so that query_command uses pc_query: Example from .muttrc:: set query_command="/home/username/bin/pc_query -m '%s'" The current version features experimental write support. If you want to test this, first make sure **you have a backup of your data** (but please do *NOT* rely on *pc_query --backup* for this just yet), then you can put the line:: write_support = YesPleaseIDoHaveABackupOfMyData in your config file (needs to be put into each *Account* section you want to enable write support for). You can also import, delete or backup single cards (backup also works for the whole collection, but please don't rely on it just yet). See *pc_query --help* for how to use these and for some more options. *pycarddav* can be configured to use different CardDAV accounts, see the example config for details. An account can be specified with *-a account_name* with all three utilies. If no account is chosen all searching and syncing actions will use all configured accounts, while on adding cards the first configured account will be used. Keyring support --------------- *pycarddav* supports keyring_, (version >=3.0). To use it, you need to add a password to the keyring via:: keyring set pycarddav:$account $username where $account is the name of an account as configured in your configuration file and $username is the corresponding username (and then have no password configured for that account). For more details on configuring keyring have a look at its documentation_. .. _keyring: https://pypi.python.org/pypi/keyring .. _documentation: https://pypi.python.org/pypi/keyring Import Addresses from Mutt -------------------------- You can directly add sender addresses from mutt to *pyCardDAV*, either adding them to existing contacts or creating a new one. If write support is enabled, they will be uploaded on the server during the next sync. Example from .muttrc:: macro index,pager A "pycard-import" "add sender address to pycardsyncer" SSL --- If you use SSL to interact with your CardDAV Server (you probably should) and you don't have a certificate signed by a CA your OS Vendor trusts (like a self-signed certificate or one signed by CAcert) you can set *verify* to a path to the CA's root file (must be in pem format). If you don't want any certificate checking set *verify* to *false* to disable *any* ssl certificate checking (this is not recommended). Conflict Resolution ------------------- In case of conflicting edits (local VCard changed while remote VCard also changed), are "resolved" by pycarddav through overwriting the local VCard with the remote one (meaning local edits are lost in this case). Syncing more frequently can prevent this. Additional Information ---------------------- For now, VCard properties that have no value are not shown. Also, you should be able to use *pyCardDAV*'s CardDAV implementation for other projects. See the *CardDAV* class in *pycarddav/carddav.py*. In accordance with RFC 6352 all VCards that are imported or changed by pyCadDAV will automatically get a random UID (if they haven't one already), as some CardDAV servers, e.g. Owncloud require these. Debian Wheezy Quickstart ------------------------ apt-get install python-requests python-vobject python-pytest python-urwid python-lxml python-pyxdg sudo python setup.py install mkdir -p ~/.config/pycard chmod 700 ~/.config/pycard cp pycard.conf.sample ~/.config/pycard/pycard.conf License ------- *pyCardDAV* is released under the Expat/MIT License: Copyright (c) 2011-2013 Christian Geier and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyCardDAV-0.6.1/setup.py0000664000175000017500000000452612245117175014027 0ustar cgcg00000000000000#!/usr/bin/env python2 import os import string import subprocess import sys import warnings #from distutils.core import setup from setuptools import setup MAJOR = 0 MINOR = 6 PATCH = 1 RELEASE = True VERSION = "{0}.{1}.{2}".format(MAJOR, MINOR, PATCH) if not RELEASE: try: try: pipe = subprocess.Popen(["git", "describe", "--dirty", "--tags"], stdout=subprocess.PIPE) except EnvironmentError: warnings.warn("WARNING: git not installed or failed to run") revision = pipe.communicate()[0].strip().lstrip('v') if pipe.returncode != 0: warnings.warn("WARNING: couldn't get git revision") if revision != VERSION: revision = revision.lstrip(string.digits + '.') VERSION += '.dev' + revision except: VERSION += '.dev' warnings.warn("WARNING: git not installed or failed to run") def write_version(): """writes the pycarddav/version.py file""" template = """\ __version__ = '{0}' """ filename = os.path.join( os.path.dirname(__file__), 'pycarddav', 'version.py') with open(filename, 'w') as versionfile: versionfile.write(template.format(VERSION)) print("wrote pycarddav/version.py with version={0}".format(VERSION)) write_version() requirements = [ 'lxml', 'vobject', 'requests', 'urwid', 'pyxdg' ] if sys.version_info[:2] in ((2, 6),): # there is no argparse in python2.6 requirements.append('argparse') setup( name='pyCardDAV', version=VERSION, description='A CardDAV based address book tool', long_description=open('README.rst').read(), author='Christian Geier', author_email='pycarddav@lostpackets.de', url='http://lostpackets.de/pycarddav/', license='Expat/MIT', packages=['pycarddav'], scripts=['bin/pycardsyncer', 'bin/pc_query', 'bin/pycard-import'], requires=requirements, install_requires=requirements, classifiers=[ "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", "Environment :: Console :: Curses", "Intended Audience :: End Users/Desktop", "Operating System :: POSIX", "Programming Language :: Python :: 2 :: Only", "Topic :: Utilities", "Topic :: Communications :: Email :: Address Book" ], ) pyCardDAV-0.6.1/CONTRIBUTING.txt0000664000175000017500000000276112245116621014757 0ustar cgcg00000000000000Submitting a Bug ================ If you found a bug or any part of pycarddav isn't working as you expected, please check if that bug is either already reported at https://github.com/geier/pycarddav/issues?state=open or is already fixed in the development branch. You can check it out and install via: git clone -b develop https://github.com/geier/pycarddav cd pycarddav python setup.py install If the bug persists, always run the command again with the --debug option and paste the output of that (of course you can edit out any private details like your username and resource). Also, it is often helpful if you include which OS you are on, which version of python and, in the case the problems occur during sync, which version of requests you are using. You can just run the file at https://gist.github.com/geier/5814123#file-debug_helper-py and paste the output. If the error occurs during sync, please also supply details on your CardDAV server (which server and version). Hacking ======= The *master* branch on github should always contain the latest released stable version. Development mostly happens in feature branches (only for major new features), the *develop* branch, or in the next point release *dev*-branch (e.g. *4.2-dev*). So please check out the *develop* branch before starting to hack on pycarddav. Before submitting your first patch, please add yourself to *CONTRIBUTORS.txt*. You can submit patches either via email (pycarddav at lostpackets dot de )or via github pull requests. pyCardDAV-0.6.1/pycard.conf.sample0000664000175000017500000000204212245116621015710 0ustar cgcg00000000000000[Account work] # DAV credentials. Please note that the password is written in plain # text here. If the password is missing, it will be claimed at # synchronization time. user: username passwd: yourpassword # The path to the CardDAV resource. #resource: https://[server]/owncloud/apps/contacts/carddav.php/addressbooks/[user]/[addressbook name]/ resource: https://carddav.server.tld:443/davical/caldav.php/username/addresses/ # Authentication Method: possible values are: basic (the default), or digest # (for servers that need HTTP digest authentification) #auth: basic # If verify is set to False, no SSL Certificate checks are done at all. Please # be aware of the security implications. The default value is True You can also # set verify to a path to your CAcert file #verify: True [Account private] user: anothername passwd: otherPasswd resource: https://domain.tld/contacts/addressbook/ [sqlite] # The location of the local SQLite contacts database. # Defaults to $XDG_DATA_HOME/pycard/abook.db #path: ~/.pycard/abook.db [default] debug: False pyCardDAV-0.6.1/pyCardDAV.egg-info/0000775000175000017500000000000012245117351015551 5ustar cgcg00000000000000pyCardDAV-0.6.1/pyCardDAV.egg-info/top_level.txt0000664000175000017500000000001212245117351020274 0ustar cgcg00000000000000pycarddav pyCardDAV-0.6.1/pyCardDAV.egg-info/SOURCES.txt0000664000175000017500000000101412245117351017431 0ustar cgcg00000000000000CONTRIBUTING.txt CONTRIBUTORS.txt COPYING MANIFEST.in NEWS.txt README.rst pycard.conf.sample setup.py bin/pc_query bin/pycard-import bin/pycardsyncer doc/man/pc_query.1 doc/man/pycard-import.1 doc/man/pycardsyncer.1 pyCardDAV.egg-info/PKG-INFO pyCardDAV.egg-info/SOURCES.txt pyCardDAV.egg-info/dependency_links.txt pyCardDAV.egg-info/requires.txt pyCardDAV.egg-info/top_level.txt pycarddav/__init__.py pycarddav/backend.py pycarddav/carddav.py pycarddav/controllers.py pycarddav/model.py pycarddav/ui.py pycarddav/version.pypyCardDAV-0.6.1/pyCardDAV.egg-info/requires.txt0000664000175000017500000000004112245117351020144 0ustar cgcg00000000000000lxml vobject requests urwid pyxdgpyCardDAV-0.6.1/pyCardDAV.egg-info/dependency_links.txt0000664000175000017500000000000112245117351021617 0ustar cgcg00000000000000 pyCardDAV-0.6.1/pyCardDAV.egg-info/PKG-INFO0000664000175000017500000002374212245117351016656 0ustar cgcg00000000000000Metadata-Version: 1.1 Name: pyCardDAV Version: 0.6.1 Summary: A CardDAV based address book tool Home-page: http://lostpackets.de/pycarddav/ Author: Christian Geier Author-email: pycarddav@lostpackets.de License: Expat/MIT Description: About ===== *pyCardDAV* is a simple to use CardDAV_ CLI client. It has built in support for mutt's *query_command* but also works very well solo. *pyCardDAV* consists of *pycardsyncer*, a program for syncing your CardDAV resource into a local database and of *pc_query*, a program for querying the local database. *pyCardDAV* is some ugly python_ code (actually, it's not *that* bad anymore…) that holds together vobject_, lxml_, requests_ and pysqlite_. .. _CardDAV: http://en.wikipedia.org/wiki/CardDAV .. _python: http://python.org/ .. _vobject: http://vobject.skyhouseconsulting.com/ .. _lxml: http://lxml.de/ .. _pysqlite: http://code.google.com/p/pysqlite/ .. _requests: http://python-requests.org Features -------- (or rather: limitations) - *pyCardDAV* can only use one address book resource at the moment - *pyCardDAV* is only tested against davical, owncloud and sabredav - *pyCardDAV* can import the sender's address directly from mutt - *pyCardDAV* can backup and import to/from .vcf files - *pyCardDAV* can add email addresses directly from mutt - *pyCardDAV* only understands VCard 3.0 - *pyCardDAV* is not python 3 compatible yet Feedback -------- Please do provide feedback if *pyCardDAV* works for you or even more importantly if it doesn't. You can reach me by email at pycarddav (at) lostpackets (dot) de , by jabber/XMPP at geier (at) jabber (dot) ccc (dot) de or via github_ .. _github: https://github.com/geier/pycarddav/ Installation ------------ You can download *pyCardDAV* either from the above download link or check it out from git (at github). Then install *pyCardDAV* by executing *python setup.py install*. If you feel more adventurous you can always the *develop* branch on github, which *should* always be in a usable state. pyCardDAV is also available on pypi_ and can be installed via pip install pycarddav or easy_install pycarddav. Copy and edit the supplied pycard.conf.sample file (default location is ~/.config/pycard/pycard.conf). If you don't want to store the password in clear text in the config file, *pyCardDAV* will ask for it while syncing. Make sure you have sqlite3 (normally available by default), vobject, lxml(>2), requests (>0.10), urwid (>0.9) pyxdg, installed. Users of python 2.6 will also need to install argparse. *pyCardDAV* has so far been successfully tested on recent versions of FreeBSD, NetBSD, Debian and Ubuntu with python 2.6 and 2.7 and against davical 0.9.9.4 - 1.0.1 (later versions should be ok, too, but 0.9.9.3 and earlier don't seem to work), owncloud and sabredav. .. _pypi: https://pypi.python.org/pypi/pyCardDAV/ .. _git: http://github.com/geier/pycarddav/ Usage ----- *pyCardDAV* consists of three scripts, *pycardsyncer* which is used to sync the local database with the server, *pc_query* to interact with the local database and *pycard-import* to import email addresses from mutt. Execute pycardsyncer to sync your addresses to the local database. You can test pc_query with:: % pc_query searchstring By default *pyCardDAV* only prints the names, email addresses and telephone numbers of contacts matching the search string, to see all vCard properties use the "-A" option. For usage with mutt etc., *pyCardDAV* can also print only email addresses in a mutt friendly format (with the "-m" option). Edit your mutt configuration so that query_command uses pc_query: Example from .muttrc:: set query_command="/home/username/bin/pc_query -m '%s'" The current version features experimental write support. If you want to test this, first make sure **you have a backup of your data** (but please do *NOT* rely on *pc_query --backup* for this just yet), then you can put the line:: write_support = YesPleaseIDoHaveABackupOfMyData in your config file (needs to be put into each *Account* section you want to enable write support for). You can also import, delete or backup single cards (backup also works for the whole collection, but please don't rely on it just yet). See *pc_query --help* for how to use these and for some more options. *pycarddav* can be configured to use different CardDAV accounts, see the example config for details. An account can be specified with *-a account_name* with all three utilies. If no account is chosen all searching and syncing actions will use all configured accounts, while on adding cards the first configured account will be used. Keyring support --------------- *pycarddav* supports keyring_, (version >=3.0). To use it, you need to add a password to the keyring via:: keyring set pycarddav:$account $username where $account is the name of an account as configured in your configuration file and $username is the corresponding username (and then have no password configured for that account). For more details on configuring keyring have a look at its documentation_. .. _keyring: https://pypi.python.org/pypi/keyring .. _documentation: https://pypi.python.org/pypi/keyring Import Addresses from Mutt -------------------------- You can directly add sender addresses from mutt to *pyCardDAV*, either adding them to existing contacts or creating a new one. If write support is enabled, they will be uploaded on the server during the next sync. Example from .muttrc:: macro index,pager A "pycard-import" "add sender address to pycardsyncer" SSL --- If you use SSL to interact with your CardDAV Server (you probably should) and you don't have a certificate signed by a CA your OS Vendor trusts (like a self-signed certificate or one signed by CAcert) you can set *verify* to a path to the CA's root file (must be in pem format). If you don't want any certificate checking set *verify* to *false* to disable *any* ssl certificate checking (this is not recommended). Conflict Resolution ------------------- In case of conflicting edits (local VCard changed while remote VCard also changed), are "resolved" by pycarddav through overwriting the local VCard with the remote one (meaning local edits are lost in this case). Syncing more frequently can prevent this. Additional Information ---------------------- For now, VCard properties that have no value are not shown. Also, you should be able to use *pyCardDAV*'s CardDAV implementation for other projects. See the *CardDAV* class in *pycarddav/carddav.py*. In accordance with RFC 6352 all VCards that are imported or changed by pyCadDAV will automatically get a random UID (if they haven't one already), as some CardDAV servers, e.g. Owncloud require these. Debian Wheezy Quickstart ------------------------ apt-get install python-requests python-vobject python-pytest python-urwid python-lxml python-pyxdg sudo python setup.py install mkdir -p ~/.config/pycard chmod 700 ~/.config/pycard cp pycard.conf.sample ~/.config/pycard/pycard.conf License ------- *pyCardDAV* is released under the Expat/MIT License: Copyright (c) 2011-2013 Christian Geier and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: License :: OSI Approved :: MIT License Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: End Users/Desktop Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 2 :: Only Classifier: Topic :: Utilities Classifier: Topic :: Communications :: Email :: Address Book Requires: lxml Requires: vobject Requires: requests Requires: urwid Requires: pyxdg pyCardDAV-0.6.1/pycarddav/0000775000175000017500000000000012245117351014257 5ustar cgcg00000000000000pyCardDAV-0.6.1/pycarddav/ui.py0000664000175000017500000003215612245116621015254 0ustar cgcg00000000000000#!/usr/bin/env python2 # vim: set ts=4 sw=4 expandtab sts=4: # Copyright (c) 2011-2013 Christian Geier & contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ The pycarddav interface to add, edit, or select a VCard. """ from __future__ import print_function try: import sys import urwid import pycarddav except ImportError, error: print(error) sys.exit(1) class VCardWalker(urwid.ListWalker): """A walker to browse a VCard list. This walker returns a selectable Text for each of the passed VCard references. Either accounts or href_account_list needs to be supplied. If no list of tuples of references are passed to the constructor, then all cards from the specified accounts are browsed. """ class Entry(urwid.Text): """A specialized Text which can be used for browsing in a list.""" _selectable = True def keypress(self, _, key): return key def __init__(self, database, accounts=None, href_account_list=None): urwid.ListWalker.__init__(self) if accounts is None and href_account_list is None: raise Exception self._db = database self._href_account_list = href_account_list or database.get_all_href_from_db(accounts) self._current = 0 @property def selected_vcard(self): """Return the focused VCard.""" return self._db.get_vcard_from_db(self._href_account_list[self._current][0], self._href_account_list[self._current][1] ) def get_focus(self): """Return (focused widget, focused position).""" return self._get_at(self._current) def set_focus(self, pos): """Focus on pos.""" self._current = pos self._modified() def get_next(self, pos): """Return (widget after pos, position after pos).""" if pos >= len(self._href_account_list) - 1: return None, None return self._get_at(pos + 1) def get_prev(self, pos): """Return (widget before pos, position before pos).""" if pos <= 0: return None, None return self._get_at(pos - 1) def _get_at(self, pos): """Return a textual representation of the VCard at pos.""" vcard = self._db.get_vcard_from_db(self._href_account_list[pos][0], self._href_account_list[pos][1] ) label = vcard.fname if vcard['EMAIL']: label += ' (%s)' % vcard['EMAIL'][0][0] return urwid.AttrMap(VCardWalker.Entry(label), 'list', 'list focused'), pos class Pane(urwid.WidgetWrap): """An abstract Pane to be used in a Window object.""" def __init__(self, widget, title=None, description=None): urwid.WidgetWrap.__init__(self, widget) self._title = title or '' self._description = description or '' self.window = None @property def title(self): return self._title @property def description(self): return self._description def get_keys(self): """Return a description of the keystrokes recognized by this pane. This method returns a list of tuples describing the keys handled by a pane. This list is used to build a contextual pane help. Each tuple is a pair of a list of keys and a description. The abstract pane returns the default keys handled by the window. Panes which do not override there keys should extend this list. """ return [(['up', 'down', 'pg.up', 'pg.down'], 'navigate through the fields.'), (['esc'], 'backtrack to the previous pane of exit.'), (['F1'], 'open this pane help.')] class HelpPane(Pane): """A contextual help screen.""" def __init__(self, pane): content = [] for key_list, description in pane.get_keys(): key_text = [] for key in key_list: if key_text: key_text.append(', ') key_text.append(('bright', key)) content.append( urwid.Columns( [urwid.Padding(urwid.Text(key_text), left=10), urwid.Padding(urwid.Text(description), right=10)])) Pane.__init__(self, urwid.ListBox(urwid.SimpleListWalker(content)), 'Help') class VCardChooserPane(Pane): """A VCards chooser. This pane allows to browse a list of VCards. If no references are passed to the constructor, then the whole database is browsed. A VCard can be selected to be used in another pane, like the EditorPane. """ def __init__(self, database, accounts=None, href_account_list=None): self._walker = VCardWalker(database, accounts=accounts, href_account_list=href_account_list) Pane.__init__(self, urwid.ListBox(self._walker), 'Browse...') def get_keys(self): keys = Pane.get_keys(self) keys.append(([' ', 'enter'], 'select a contact.')) return keys def keypress(self, size, key): self._w.keypress(size, key) if key in ['space', 'enter']: self.window.backtrack(self._walker.selected_vcard) else: return key class EditorPane(Pane): """A VCard editor.""" def __init__(self, database, account, vcard): self._vcard = vcard self._db = database self._account = account self._label = vcard.fname if vcard.fname else vcard['EMAIL'][0][0] self._fname_edit = urwid.Edit(u'', u'') self._lname_edit = urwid.Edit(u'', u'') self._email_edits = None Pane.__init__(self, self._build_ui(), 'Edit %s' % vcard.fname) def get_keys(self): keys = Pane.get_keys(self) keys.append((['F8'], 'save this contact.')) return keys def keypress(self, size, key): self._w.keypress(size, key) if key == 'f8': self._validate() self.window.backtrack() else: return key def on_button_press(self, button): if button.get_label() == 'Merge': self.window.open(VCardChooserPane(self._db, accounts=[self._account]), self.on_merge_vcard) else: if button.get_label() == 'Store': self._validate() self.window.backtrack() def on_merge_vcard(self, vcard): # TODO: this currently merges only one email field, which is ok to use with mutt. if vcard: vcard['EMAIL'].append(self._vcard['EMAIL'][0]) self._vcard = vcard self._w = self._build_ui() self._status = pycarddav.backend.CHANGED def _build_ui(self): content = [] content.extend(self._build_names_section()) content.extend(self._build_emails_section()) content.extend(self._build_buttons_section()) return urwid.ListBox(urwid.SimpleListWalker(content)) def _build_names_section(self): names = self._vcard.name.split(';') if len(names) > 1: self._lname_edit.set_edit_text(names[0]) self._fname_edit.set_edit_text(names[1]) else: self._lname_edit.set_edit_text(u'') self._fname_edit.set_edit_text(names[0]) return [urwid.Divider(), urwid.Columns([ ('fixed', 15, urwid.AttrWrap(urwid.Text(u'First Name'), 'line header')), urwid.AttrWrap(self._fname_edit, 'edit', 'edit focused')]), urwid.Divider(), urwid.Columns([ ('fixed', 15, urwid.AttrWrap(urwid.Text(u'Last Name'), 'line header')), urwid.AttrWrap(self._lname_edit, 'edit', 'edit focused')])] def _build_emails_section(self): self._email_edits = [] content = [] for mail in self._vcard['EMAIL']: edit = urwid.Edit('', mail[0]) self._email_edits.append(edit) content.extend([ urwid.Divider(), urwid.Columns([ ('fixed', 15, urwid.AttrWrap(urwid.Text(u'Email'), 'line header')), urwid.AttrWrap(edit, 'edit', 'edit focused')])]) return content def _build_buttons_section(self): buttons = [u'Cancel', u'Merge', u'Store'] row = urwid.GridFlow([urwid.AttrWrap(urwid.Button(lbl, self.on_button_press), 'button', 'button focused') for lbl in buttons], 10, 3, 1, 'left') return [urwid.Divider('-', 1, 1), urwid.Padding(row, 'right', 13 * len(buttons), None, 1, 1)] def _validate(self): self._vcard.fname = ' '.join( [self._fname_edit.edit_text, self._lname_edit.edit_text]) for i, edit in enumerate(self._email_edits): self._vcard['EMAIL'][i] = (edit.edit_text, self._vcard['EMAIL'][i][1]) if(hasattr(self, '_status')): status = self._status else: status = pycarddav.backend.NEW self._db.update(self._vcard, self._account, self._vcard.href, etag=self._vcard.etag, status=status) class Window(urwid.Frame): """The main user interface frame. A window is a frame which displays a header, a footer and a body. The header and the footer are handled by this object, and the body is the space where Panes can be displayed. Each Pane is an interface to interact with the database in one way: list the VCards, edit one VCard, and so on. The Window provides a mechanism allowing the panes to chain themselves, and to carry data between them. """ PALETTE = [('header', 'white', 'black'), ('footer', 'white', 'black'), ('line header', 'black', 'white', 'bold'), ('bright', 'dark blue', 'white', ('bold', 'standout')), ('list', 'black', 'white'), ('list focused', 'white', 'light blue', 'bold'), ('edit', 'black', 'white'), ('edit focused', 'white', 'light blue', 'bold'), ('button', 'black', 'dark cyan'), ('button focused', 'white', 'light blue', 'bold')] def __init__(self): self._track = [] self._title = u' %s v.%s' % (pycarddav.__productname__, pycarddav.__version__) header = urwid.AttrWrap(urwid.Text(self._title), 'header') footer = urwid.AttrWrap(urwid.Text( u' Use Up/Down/PgUp/PgDown to scroll. Esc to return. F1 for help'), 'footer') urwid.Frame.__init__(self, urwid.Text(''), header=header, footer=footer) def open(self, pane, callback=None): """Open a new pane. The given pane is added to the track and opened. If the given callback is not None, it will be called when this new pane will be closed. """ pane.window = self self._track.append((pane, callback)) self._update(pane) def backtrack(self, data=None): """Unstack the displayed pane. The current pane is discarded, and the previous one is displayed. If the current pane was opened with a callback, this callback is called with the given data (if any) before the previous pane gets redrawn. """ _, cb = self._track.pop() if cb: cb(data) if self._track: self._update(self._get_current_pane()) else: raise urwid.ExitMainLoop() def on_key_press(self, key): """Handle application-wide key strokes.""" if key == 'esc': self.backtrack() elif key == 'f1': self.open(HelpPane(self._get_current_pane())) def _update(self, pane): self.header.w.set_text(u'%s | %s' % (self._title, pane.title)) self.set_body(pane) def _get_current_pane(self): return self._track[-1][0] if self._track else None def start_pane(pane): """Open the user interface with the given initial pane.""" frame = Window() frame.open(pane) loop = urwid.MainLoop(frame, Window.PALETTE, unhandled_input=frame.on_key_press) loop.run() pyCardDAV-0.6.1/pycarddav/controllers.py0000664000175000017500000002046312245116621017203 0ustar cgcg00000000000000#!/usr/bin/env python2 # coding: utf-8 # vim: set ts=4 sw=4 expandtab sts=4: # Copyright (c) 2011-2013 Christian Geier & contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ syncs the remote database to the local db """ from pycarddav import backend from pycarddav import carddav from pycarddav import model from pycarddav import ui from os import path import logging import sys def query(conf): # testing if the db exists if not path.exists(path.expanduser(conf.sqlite.path)): sys.exit(str(conf.sqlite.path) + " file does not exist, please sync" " with pycardsyncer first.") search_string = conf.query.search_string.decode("utf-8") my_dbtool = backend.SQLiteDb(db_path=path.expanduser(conf.sqlite.path), encoding="utf-8", errors="stricts", debug=False) #import: if conf.query.importing: cards = model.cards_from_file(conf.query.importing) for card in cards: my_dbtool.update(card, conf.sync.accounts[0], status=backend.NEW) sys.exit() # backup: if conf.query.backup: with open(conf.query.backup, 'w') as vcf_file: if search_string == "": href_account_list = my_dbtool.get_all_href_from_db( conf.sync.accounts) else: href_account_list = my_dbtool.search(search_string, conf.sync.accounts) for href, account in href_account_list: vcard = my_dbtool.get_vcard_from_db(href, account) vcf_file.write(vcard.vcf.encode('utf-8')) sys.exit() # editing a card: #if conf.query.edit: # names = my_dbtool.select_entry2(search_string) # href = ui.select_entry(names) # if href is None: # sys.exit("Found no matching cards.") # mark a card for deletion if conf.query.delete: href_account_list = my_dbtool.search(search_string, conf.sync.accounts) if len(href_account_list) is 0: sys.exit('Found no matching cards.') elif len(href_account_list) is 1: href, account = href_account_list[0] card = my_dbtool.get_vcard_from_db(href, account) else: pane = ui.VCardChooserPane(my_dbtool, href_account_list=href_account_list) ui.start_pane(pane) card = pane._walker.selected_vcard if card.href in my_dbtool.get_new(card.account): # cards not yet on the server get deleted directly, otherwise we # will try to delete them on the server later (where they don't # exist) and this will raise an exception my_dbtool.delete_vcard_from_db(card.href, card.account) else: my_dbtool.mark_delete(card.href, card.account) print(u'vcard {0} - "{1}" deleted from local db, ' 'will be deleted on the server on the next ' 'sync'.format(card.href, card.fname)) sys.exit() print("searching for " + conf.query.search_string + "...") result = my_dbtool.search(search_string, conf.sync.accounts) for href, account in result: vcard = my_dbtool.get_vcard_from_db(href, account) if conf.query.mutt_format: lines = vcard.print_email() elif conf.query.tel: lines = vcard.print_tel() elif conf.query.display_all: lines = vcard.pretty else: lines = vcard.pretty_min if not lines == '': print(lines.encode('utf-8')) return 0 def sync(conf): """this should probably be seperated from the class definitions""" syncer = carddav.PyCardDAV(conf.account.resource, user=conf.account.user, passwd=conf.account.passwd, write_support=conf.account.write_support, verify=conf.account.verify, auth=conf.account.auth) my_dbtool = backend.SQLiteDb(db_path=conf.sqlite.path, encoding="utf-8", errors="stricts", debug=conf.debug) # sync: abook = syncer.get_abook() # type(abook): dict my_dbtool.check_account_table(conf.account.name, conf.account.resource) for href, etag in abook.iteritems(): if my_dbtool.needs_update(href, conf.account.name, etag=etag): logging.debug("getting %s etag: %s", href, etag) vcard = syncer.get_vcard(href) my_dbtool.update(vcard, conf.account.name, href=href, etag=etag) remote_changed = False # for now local changes overwritten by remote changes logging.debug("looking for locally changed vcards...") hrefs = my_dbtool.get_changed(conf.account.name) for href in hrefs: try: logging.debug("trying to update %s", href) card = my_dbtool.get_vcard_from_db(href, conf.account.name) logging.debug("%s", my_dbtool.get_etag(href, conf.account.name)) syncer.update_vcard(card.vcf, href, None) my_dbtool.reset_flag(href, conf.account.name) remote_changed = True except carddav.NoWriteSupport: logging.info('failed to upload changed card {0}, ' 'you need to enable write support, ' 'see the documentation', href) # uploading hrefs = my_dbtool.get_new(conf.account.name) for href in hrefs: try: logging.debug("trying to upload new card %s", href) card = my_dbtool.get_vcard_from_db(href, conf.account.name) (href_new, etag_new) = syncer.upload_new_card(card.vcf) my_dbtool.update_href(href, href_new, conf.account.name, status=backend.OK) remote_changed = True except carddav.NoWriteSupport: logging.info('failed to upload card %s, ' 'you need to enable write support, ' 'see the documentation', href) # deleting locally deleted cards on the server hrefs_etags = my_dbtool.get_marked_delete(conf.account.name) for href, etag in hrefs_etags: try: logging.debug('trying to delete card %s', href) syncer.delete_vcard(href, etag) my_dbtool.delete_vcard_from_db(href, conf.account.name) remote_changed = True except carddav.NoWriteSupport: logging.info('failed to delete card {0}, ' 'you need to enable write support, ' 'see the documentation'.format(href)) # detecting remote-deleted cards # is there a better way to compare a list of unicode() with a list of str() # objects? if remote_changed: abook = syncer.get_abook() # type (abook): dict r_href_account_list = my_dbtool.get_all_href_from_db_not_new( [conf.account.name]) delete = set([href for href, account in r_href_account_list]).difference(abook.keys()) for href in delete: my_dbtool.delete_vcard_from_db(href, conf.account.name) pyCardDAV-0.6.1/pycarddav/model.py0000664000175000017500000002305512245116621015735 0ustar cgcg00000000000000#!/usr/bin/env python2 # vim: set ts=4 sw=4 expandtab sts=4: # Copyright (c) 2011-2013 Christian Geier & contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ The pycarddav abstract model and tools for VCard handling. """ from __future__ import print_function try: import base64 import sys import vobject from collections import defaultdict except ImportError, error: print(error) sys.exit(1) def list_clean(string): """ transforms a comma seperated string to a list, stripping whitespaces "HOME, WORK,pref" -> ['HOME', 'WORK', 'pref'] string: string of comma seperated elements returns: list() """ string = string.split(',') rstring = list() for element in string: rstring.append(element.strip(' ')) return rstring NO_STRINGS = [u"n", "n", u"no", "no"] YES_STRINGS = [u"y", "y", u"yes", "yes"] PROPERTIES = ['EMAIL', 'TEL'] PROPS_ALL = ['FN', 'N', 'VERSION', 'NICKNAME', 'PHOTO', 'BDAY', 'ADR', 'LABEL', 'TEL', 'EMAIL', 'MAILER', 'TZ', 'GEO', 'TITLE', 'ROLE', 'LOGO', 'AGENT', 'ORG', 'NOTE', 'REV', 'SOUND', 'URL', 'UID', 'KEY', 'CATEGORIES', 'PRODID', 'REV', 'SORT-STRING', 'SOUND', 'URL', 'VERSION', 'UTC-OFFSET'] PROPS_ALLOWED = ['NICKNAME', 'BDAY', 'ADR', 'LABEL', 'TEL', 'EMAIL', 'MAILER', 'TZ', 'GEO', 'TITLE', 'ROLE', 'AGENT', 'ORG', 'NOTE', 'REV', 'SOUND', 'URL', 'UID', 'KEY', 'CATEGORIES', 'PRODID', 'REV', 'SORT-STRING', 'SOUND', 'URL', 'VERSION', 'UTC-OFFSET'] PROPS_ONCE = ['FN', 'N', 'VERSION'] PROPS_LIST = ['NICKNAME', 'CATEGORIES'] PROPS_BIN = ['PHOTO', 'LOGO', 'SOUND', 'KEY'] RTEXT = '\x1b[7m' NTEXT = '\x1b[0m' BTEXT = '\x1b[1m' def get_names(display_name): first_name, last_name = '', display_name if display_name.find(',') > 0: # Parsing something like 'Doe, John Abraham' last_name, first_name = display_name.split(',') elif display_name.find(' '): # Parsing something like 'John Abraham Doe' # TODO: This fails for compound names. What is the most common case? name_list = display_name.split(' ') last_name = ''.join(name_list[-1]) first_name = ' '.join(name_list[:-1]) return first_name.strip().capitalize(), last_name.strip().capitalize() def vcard_from_vobject(vcard): vdict = VCard() if vcard.name != "VCARD": raise Exception # TODO proper Exception type for line in vcard.getChildren(): # this might break, was tried/excepted before line.transformFromNative() property_name = line.name property_value = line.value try: if line.ENCODING_paramlist == [u'b']: property_value = base64.b64encode(line.value) except AttributeError: pass if type(property_value) == list: property_value = (',').join(property_value) vdict[property_name].append((property_value, line.params,)) return vdict def vcard_from_string(vcard_string): """ vcard_string: str() or unicode() returns VCard() """ try: vcard = vobject.readOne(vcard_string) except vobject.base.ParseError as error: raise Exception(error) # TODO proper exception return vcard_from_vobject(vcard) def vcard_from_email(display_name, email): fname, lname = get_names(display_name) vcard = vobject.vCard() vcard.add('n') vcard.n.value = vobject.vcard.Name(family=lname, given=fname) vcard.add('fn') vcard.fn.value = display_name vcard.add('email') vcard.email.value = email vcard.email.type_param = 'INTERNET' return vcard_from_vobject(vcard) def cards_from_file(cards_f): collector = list() for vcard in vobject.readComponents(cards_f): collector.append(vcard_from_vobject(vcard)) return collector class VCard(defaultdict): """ internal representation of a VCard. This is dict with some associated methods, each dict item is a list of tuples i.e.: >>> vcard['EMAIL'] [('hanz@wurst.com', ['WORK', 'PREF']), ('hanz@wurst.net', ['HOME'])] self.href: unique id (really just the url) of the VCard self.account: account which this card is associated with db_path: database file from which to initialize the VCard self.edited: 0: nothing changed 1: name and/or fname changed 2: some property was deleted """ def __init__(self, ddict=''): if ddict == '': defaultdict.__init__(self, list) else: defaultdict.__init__(self, list, ddict) self.href = '' self.account = '' self.etag = '' self.edited = 0 def serialize(self): return self.items().__repr__() @property def name(self): return unicode(self['N'][0][0]) if self['N'] else '' @name.setter def name(self, value): if not self['N']: self['N'] = [('', {})] self['N'][0][0] = value @property def fname(self): return unicode(self['FN'][0][0]) if self['FN'] else '' @fname.setter def fname(self, value): self['FN'][0] = (value, {}) def alt_keys(self): keylist = self.keys() for one in [x for x in ['FN', 'N', 'VERSION'] if x in keylist]: keylist.remove(one) keylist.sort() return keylist def print_email(self): """prints only name, email and type for use with mutt""" collector = list() try: for one in self['EMAIL']: try: typelist = ','.join(one[1][u'TYPE']) except KeyError: typelist = '' collector.append(one[0] + "\t" + self.fname + "\t" + typelist) return '\n'.join(collector) except KeyError: return '' def print_tel(self): """prints only name, email and type for use with mutt""" collector = list() try: for one in self['TEL']: try: typelist = ','.join(one[1][u'TYPE']) except KeyError: typelist = '' collector.append(self.fname + "\t" + one[0] + "\t" + typelist) return '\n'.join(collector) except KeyError: return '' @property def pretty(self): return self._pretty_base(self.alt_keys()) @property def pretty_min(self): return self._pretty_base(['TEL', 'EMAIL']) def _pretty_base(self, keylist): collector = list() if sys.stdout.isatty(): collector.append('\n' + BTEXT + 'Name: ' + self.fname + NTEXT) else: collector.append('\n' + 'Name: ' + self.fname) for key in keylist: for value in self[key]: try: types = ' (' + ', '.join(value[1]['TYPE']) + ')' except KeyError: types = '' try: line = key + types + ': ' + value[0] except UnicodeDecodeError: line = key + types + ': ' + '' collector.append(line) return '\n'.join(collector) def _line_helper(self, line): collector = list() for key in line[1].keys(): collector.append(key + '=' + ','.join(line[1][key])) if collector == list(): return '' else: return (';' + ';'.join(collector)) @property def vcf(self): """serialize to VCARD as specified in RFC2624, if no UID is specified yet, one will be added (as a UID is mandatory for carddav as specified in RF6352 TODO make shure this random uid is unique""" import string import random def generate_random_uid(): """generate a random uid, when random isn't broken, getting a random UID from a pool of roughly 10^56 should be good enough""" choice = string.ascii_uppercase + string.digits return ''.join([random.choice(choice) for _ in range(36)]) if 'UID' not in self.keys(): self['UID'] = [(generate_random_uid(), dict())] collector = list() collector.append('BEGIN:VCARD') collector.append('VERSION:3.0') for key in ['FN', 'N']: collector.append(key + ':' + self[key][0][0]) for prop in self.alt_keys(): for line in self[prop]: types = self._line_helper(line) collector.append(prop + types + ':' + line[0]) collector.append('END:VCARD') return '\n'.join(collector) pyCardDAV-0.6.1/pycarddav/carddav.py0000664000175000017500000002416112245116621016240 0ustar cgcg00000000000000#!/usr/bin/env python2 # vim: set ts=4 sw=4 expandtab sts=4: # Copyright (c) 2011-2013 Christian Geier & contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ contains the class PyCardDAv and some associated functions and definitions """ from collections import namedtuple import requests import urlparse import logging import lxml.etree as ET def get_random_href(): """returns a random href""" import random tmp_list = list() for _ in xrange(3): rand_number = random.randint(0, 0x100000000) tmp_list.append("{0:x}".format(rand_number)) return "-".join(tmp_list).upper() class UploadFailed(Exception): """uploading the card failed""" pass class NoWriteSupport(Exception): """write support has not been enabled""" pass class PyCardDAV(object): """class for interacting with a CardDAV server Since PyCardDAV relies heavily on Requests [1] its SSL verification is also shared by PyCardDAV [2]. For now, only the *verify* keyword is exposed through PyCardDAV. [1] http://docs.python-requests.org/ [2] http://docs.python-requests.org/en/latest/user/advanced/ raises: requests.exceptions.SSLError requests.exceptions.ConnectionError more requests.exceptions depending on the actual error Exception (shame on me) """ def __init__(self, resource, debug='', user='', passwd='', verify=True, write_support=False, auth='basic'): #shutup urllib3 urllog = logging.getLogger('requests.packages.urllib3.connectionpool') urllog.setLevel(logging.CRITICAL) urllog = logging.getLogger('urllib3.connectionpool') urllog.setLevel(logging.CRITICAL) split_url = urlparse.urlparse(resource) url_tuple = namedtuple('url', 'resource base path') self.url = url_tuple(resource, split_url.scheme + '://' + split_url.netloc, split_url.path) self.debug = debug self.session = requests.session() self.write_support = write_support self._settings = {'verify': verify} if auth == 'basic': self._settings['auth'] = (user, passwd,) if auth == 'digest': from requests.auth import HTTPDigestAuth self._settings['auth'] = HTTPDigestAuth(user, passwd) self._default_headers = {"User-Agent": "pyCardDAV"} headers = self.headers headers['Depth'] = '1' response = self.session.request('OPTIONS', self.url.resource, headers=headers, **self._settings) response.raise_for_status() # raises error on not 2XX HTTP status code if response.headers['DAV'].count('addressbook') == 0: raise Exception("URL is not a CardDAV resource") @property def verify(self): """gets verify from settings dict""" return self._settings['verify'] @verify.setter def verify(self, verify): """set verify""" self._settings['verify'] = verify @property def headers(self): """returns the headers""" return dict(self._default_headers) def _check_write_support(self): """checks if user really wants his data destroyed""" if not self.write_support: raise NoWriteSupport def get_abook(self): """does the propfind and processes what it returns :rtype: list of hrefs to vcards """ xml = self._get_xml_props() abook = self._process_xml_props(xml) return abook def get_vcard(self, href): """ pulls vcard from server :returns: vcard :rtype: string """ response = self.session.get(self.url.base + href, headers=self.headers, **self._settings) response.raise_for_status() return response.content def update_vcard(self, card, href, etag): """ pushes changed vcard to the server card: vcard as unicode string etag: str or None, if this is set to a string, card is only updated if remote etag matches. If etag = None the update is forced anyway """ # TODO what happens if etag does not match? self._check_write_support() remotepath = str(self.url.base + href) headers = self.headers headers['content-type'] = 'text/vcard' if etag is not None: headers['If-Match'] = etag self.session.put(remotepath, data=card, headers=headers, **self._settings) def delete_vcard(self, href, etag): """deletes vcard from server deletes the resource at href if etag matches, if etag=None delete anyway :param href: href of card to be deleted :type href: str() :param etag: etag of that card, if None card is always deleted :type href: str() :returns: nothing """ # TODO: what happens if etag does not match, url does not exist etc ? self._check_write_support() remotepath = str(self.url.base + href) headers = self.headers headers['content-type'] = 'text/vcard' if etag is not None: headers['If-Match'] = etag response = self.session.delete(remotepath, headers=headers, **self._settings) response.raise_for_status() def upload_new_card(self, card): """ upload new card to the server :param card: vcard to be uploaded :type card: unicode :rtype: tuple of string (path of the vcard on the server) and etag of new card (string or None) """ self._check_write_support() card = card.encode('utf-8') for _ in range(0, 5): rand_string = get_random_href() remotepath = str(self.url.resource + rand_string + ".vcf") headers = self.headers headers['content-type'] = 'text/vcard' # TODO perhaps this should # be set to the value this carddav server uses itself headers['If-None-Match'] = '*' response = requests.put(remotepath, data=card, headers=headers, **self._settings) if response.ok: parsed_url = urlparse.urlparse(remotepath) if 'etag' not in response.headers.keys() or response.headers['etag'] is None: etag = '' else: etag = response.headers['etag'] return (parsed_url.path, etag) response.raise_for_status() def _get_xml_props(self): """PROPFIND method gets the xml file with all vcard hrefs :rtype: str() (an xml file) """ headers = self.headers headers['Depth'] = '1' response = self.session.request('PROPFIND', self.url.resource, headers=headers, **self._settings) response.raise_for_status() return response.content @classmethod def _process_xml_props(cls, xml): """processes the xml from PROPFIND, listing all vcard hrefs :param xml: the xml file :type xml: str() :rtype: dict() key: href, value: etag """ namespace = "{DAV:}" element = ET.XML(xml) abook = dict() for response in element.iterchildren(): if (response.tag == namespace + "response"): href = "" etag = "" insert = False for refprop in response.iterchildren(): if (refprop.tag == namespace + "href"): href = refprop.text for prop in refprop.iterchildren(): for props in prop.iterchildren(): # different servers give different getcontenttypes: # e.g.: # "text/vcard" # "text/x-vcard" # "text/x-vcard; charset=utf-8" # "text/directory;profile=vCard" # "text/directory" # "text/vcard; charset=utf-8" CalendarServer if (props.tag == namespace + "getcontenttype" and props.text.split(';')[0].strip() in ['text/vcard', 'text/x-vcard']): insert = True if (props.tag == namespace + "resourcetype" and namespace + "collection" in [c.tag for c in props.iterchildren()]): insert = False break if (props.tag == namespace + "getetag"): etag = props.text if insert: abook[href] = etag return abook pyCardDAV-0.6.1/pycarddav/backend.py0000664000175000017500000003571212245116621016227 0ustar cgcg00000000000000#!/usr/bin/env python2 # vim: set ts=4 sw=4 expandtab sts=4: # Copyright (c) 2011-2013 Christian Geier & contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ The SQLite backend implementation. Database Layout =============== current version number: 9 tables: version, accounts, account_$ACCOUNTNAME version: version (INT): only one line: current db version account: account (TEXT): name of the account resource (TEXT) account_$ACCOUNTNAME: href (TEXT) etag (TEXT) name (TEXT): name as in vcard, seperated by ';' fname (TEXT): formated name status (INT): status of this card, see below for meaning vcard (TEXT): the actual vcard """ from __future__ import print_function try: from pycarddav import model import xdg.BaseDirectory import sys import sqlite3 import logging from os import path except ImportError, error: print(error) sys.exit(1) OK = 0 # not touched since last sync NEW = 1 # new card, needs to be created on the server CHANGED = 2 # properties edited or added (news to be pushed to server) DELETED = 9 # marked for deletion (needs to be deleted on server) class SQLiteDb(object): """Querying the addressbook database the type() of parameters named "account" should be something like str() and of parameters named "accountS" should be an iterable like list() """ def __init__(self, db_path=None, encoding="utf-8", errors="strict", debug=False): if db_path is None: db_path = xdg.BaseDirectory.save_data_path('pycard') + 'abook.db' self.db_path = path.expanduser(db_path) self.conn = sqlite3.connect(self.db_path) self.cursor = self.conn.cursor() self.encoding = encoding self.errors = errors self.debug = debug self.display_all = False self.print_function = "print_contact_info" self._create_default_tables() self._check_table_version() def __del__(self): self.conn.close() def search(self, search_string, accounts): """returns list of ids from db matching search_string""" stuple = ('%' + search_string + '%', ) result = list() for account in accounts: sql_s = 'SELECT href FROM {0} WHERE vcard LIKE (?)'.format(account) hrefs = self.sql_ex(sql_s, stuple) result = result + ([(href[0], account) for href in hrefs]) return result def _dump(self, account_name): """return table self.account, used for testing""" sql_s = 'SELECT * FROM {0}'.format(account_name) result = self.sql_ex(sql_s) return result def _check_table_version(self): """tests for current db Version if the table is still empty, insert db_version """ database_version = 10 # the current db VERSION self.cursor.execute('SELECT version FROM version') result = self.cursor.fetchone() if result is None: stuple = (database_version, ) # database version db Version self.cursor.execute('INSERT INTO version (version) VALUES (?)', stuple) self.conn.commit() elif not result[0] == database_version: raise Exception(str(self.db_path) + " is probably an invalid or outdated database.\n" "You should consider to remove it and sync again " "using pycardsyncer.\n") def _create_default_tables(self): """creates version and account tables and insert table version number """ # CREATE TABLE IF NOT EXISTS is faster than checking if it exists try: self.cursor.execute('''CREATE TABLE IF NOT EXISTS version ( version INTEGER )''') logging.debug("made sure version table exists") except Exception as error: sys.stderr.write('Failed to connect to database,' 'Unknown Error: ' + str(error) + "\n") self.conn.commit() try: self.cursor.execute('''CREATE TABLE IF NOT EXISTS accounts ( account TEXT NOT NULL, resource TEXT NOT NULL )''') logging.debug("made sure accounts table exists ") except Exception as error: sys.stderr.write('Failed to connect to database,' 'Unknown Error: ' + str(error) + "\n") self.conn.commit() self._check_table_version() # insert table version def sql_ex(self, statement, stuple=''): """wrapper for sql statements, does a "fetchall" """ self.cursor.execute(statement, stuple) result = self.cursor.fetchall() self.conn.commit() return result def check_account_table(self, account_name, resource): count_sql_s = """SELECT count(*) FROM accounts WHERE account = ? AND resource = ?""" self.cursor.execute(count_sql_s, (account_name, resource)) result = self.cursor.fetchone() if(result[0] != 0): return sql_s = """CREATE TABLE IF NOT EXISTS {0} ( href TEXT, etag TEXT, name TEXT, fname TEXT, vcard TEXT, status INT NOT NULL, PRIMARY KEY(href) )""".format(account_name) self.sql_ex(sql_s) sql_s = 'INSERT INTO accounts (account, resource) VALUES (?, ?)' self.sql_ex(sql_s, (account_name, resource)) logging.debug("made sure {0} table exists".format(account_name)) def needs_update(self, href, account_name, etag=''): """checks if we need to update this vcard if no table with the name account_$ACCOUNT exists, it will be created :param href: href of vcard :type href: str() :param etag: etag of vcard :type etag: str() :return: True or False """ stuple = (href,) sql_s = 'SELECT etag FROM {0} WHERE href = ?'.format(account_name) result = self.sql_ex(sql_s, stuple) if len(result) is 0: return True elif etag != result[0][0]: return True else: return False def update(self, vcard, account_name, href='', etag='', status=OK): """insert a new or update an existing card in the db :param vcard: vcard to be inserted or updated :type vcard: model.VCard() or unicode() (an actual vcard) :param href: href of the card on the server, if this href already exists in the db the card gets updated. If no href is given, a random href is chosen and it is implied that this card does not yet exist on the server, but will be uploaded there on next sync. :type href: str() :param etag: the etga of the vcard, if this etag does not match the remote etag on next sync, this card will be updated from the server. For locally created vcards this should not be set :type etag: str() :param status: status of the vcard * OK: card is in sync with remote server * NEW: card is not yet on the server, this needs to be set for locally created vcards * CHANGED: card locally changed, will be updated on the server on next sync (if remote card has not changed since last sync) * DELETED: card locally delete, will also be deleted on one the server on next sync (if remote card has not changed) :type status: one of backend.OK, backend.NEW, backend.CHANGED, BACKEND.DELETED """ if isinstance(vcard, (str, unicode)): # unicode for py2, str for py3 try: vcard_s = vcard.decode('utf-8') except UnicodeEncodeError: vcard_s = vcard # incase it's already unicode and py2 try: vcard = model.vcard_from_string(vcard) except: logging.error('VCard {0} could not be inserted into the ' 'db'.format(href)) if self.debug: logging.error('could not be converted to vcard') logging.error(vcard) return else: vcard_s = vcard.vcf if href == '': href = get_random_href() stuple = (etag, vcard.name, vcard.fname, vcard_s, status, href, href) sql_s = ('INSERT OR REPLACE INTO {0} ' '(etag, name, fname, vcard, status, href) ' 'VALUES (?, ?, ?, ?, ?, ' 'COALESCE((SELECT href FROM {0} WHERE href = ?), ?)' ');'.format(account_name)) self.sql_ex(sql_s, stuple) def update_href(self, old_href, new_href, account_name, etag='', status=OK): """updates old_href to new_href, can also alter etag and status, see update() for an explanation of these parameters""" stuple = (new_href, etag, status, old_href) sql_s = 'UPDATE {0} SET href = ?, etag = ?, status = ? \ WHERE href = ?;'.format(account_name) self.sql_ex(sql_s, stuple) def href_exists(self, href, account_name): """returns True if href already exist in db :param href: href :type href: str() :returns: True or False """ sql_s = 'SELECT href FROM {0} WHERE href = ?;'.format(account_name) if len(self.sql_ex(sql_s, (href, ))) == 0: return False else: return True def get_etag(self, href, account_name): """get etag for href type href: str() return: etag rtype: str() """ sql_s = 'SELECT etag FROM {0} WHERE href=(?);'.format(account_name) etag = self.sql_ex(sql_s, (href,))[0][0] return etag def delete_vcard_from_db(self, href, account_name): """ removes the whole vcard, returns nothing """ stuple = (href, ) logging.debug("locally deleting " + str(href)) self.sql_ex('DELETE FROM {0} WHERE href=(?)'.format(account_name), stuple) def get_all_href_from_db(self, accounts): """returns a list with all hrefs """ result = list() for account in accounts: hrefs = self.sql_ex('SELECT href FROM {0} ORDER BY fname ' 'COLLATE NOCASE'.format(account)) result = result + [(href[0], account) for href in hrefs] return result def get_all_href_from_db_not_new(self, accounts): """returns list of all not new hrefs""" result = list() for account in accounts: sql_s = 'SELECT href FROM {0} WHERE status != (?)'.format(account) stuple = (NEW,) hrefs = self.sql_ex(sql_s, stuple) result = result + [(href[0], account) for href in hrefs] return result # def get_names_href_from_db(self, searchstring=None): # """ # :return: list of tuples(name, href) of all entries from the db # """ # if searchstring is None: # return self.sql_ex('SELECT fname, href FROM {0} ' # 'ORDER BY name'.format(self.account)) # else: # hrefs = self.search(searchstring) # temp = list() # for href in hrefs: # try: # sql_s = 'SELECT fname, href FROM {0} WHERE href =(?)'.format(self.account) # result = self.sql_ex(sql_s, (href, )) # temp.append(result[0]) # except IndexError as error: # print(href) # print(error) # return temp def get_vcard_from_db(self, href, account_name): """returns a VCard() """ sql_s = 'SELECT vcard, etag FROM {0} WHERE href=(?)'.format(account_name) result = self.sql_ex(sql_s, (href, )) vcard = model.vcard_from_string(result[0][0]) vcard.href = href vcard.account = account_name vcard.etag = result[0][1] return vcard def get_changed(self, account_name): """returns list of hrefs of locally edited vcards """ sql_s = 'SELECT href FROM {0} WHERE status == (?)'.format(account_name) result = self.sql_ex(sql_s, (CHANGED, )) return [row[0] for row in result] def get_new(self, account_name): """returns list of hrefs of locally added vcards """ sql_s = 'SELECT href FROM {0} WHERE status == (?)'.format(account_name) result = self.sql_ex(sql_s, (NEW, )) return [row[0] for row in result] def get_marked_delete(self, account_name): """returns list of tuples (hrefs, etags) of locally deleted vcards """ sql_s = 'SELECT href, etag FROM {0} WHERE status == (?)'.format(account_name) result = self.sql_ex(sql_s, (DELETED, )) return result def mark_delete(self, href, account_name): """marks the entry as to be deleted on server on next sync """ sql_s = 'UPDATE {0} SET STATUS = ? WHERE href = ?'.format(account_name) self.sql_ex(sql_s, (DELETED, href, )) def reset_flag(self, href, account_name): """ resets the status for a given href to 0 (=not edited locally) """ sql_s = 'UPDATE {0} SET status = ? WHERE href = ?'.format(account_name) self.sql_ex(sql_s, (OK, href, )) def get_random_href(): """returns a random href """ import random tmp_list = list() for _ in xrange(3): rand_number = random.randint(0, 0x100000000) tmp_list.append("{0:x}".format(rand_number)) return "-".join(tmp_list).upper() pyCardDAV-0.6.1/pycarddav/version.py0000644000175000017500000000002612245117351016312 0ustar cgcg00000000000000__version__ = '0.6.1' pyCardDAV-0.6.1/pycarddav/__init__.py0000664000175000017500000004072712245116621016401 0ustar cgcg00000000000000#!/usr/bin/env python2 # vim: set fileencoding=utf-8 : # Copyright (c) 2011-2013 Christian Geier & contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. import argparse import ConfigParser import getpass import re import logging import os import signal import sys import xdg.BaseDirectory import version from netrc import netrc from urlparse import urlsplit __productname__ = 'pyCardDAV' __version__ = version.__version__ __author__ = 'Christian Geier' __copyright__ = 'Copyright 2011-2013 Christian Geier & contributors' __author_email__ = 'pycarddav@lostpackets.de' __description__ = 'A CardDAV based address book tool' __license__ = 'Expat/MIT, see COPYING' __homepage__ = 'http://lostpackets.de/pycarddav/' def capture_user_interruption(): """ Tries to hide to the user the ugly python backtraces generated by pressing Ctrl-C. """ signal.signal(signal.SIGINT, lambda x, y: sys.exit(0)) class Namespace(dict): """The pycarddav configuration holder. This holder is a dict subclass that exposes its items as attributes. Inspired by NameSpace from argparse, Configuration is a simple object providing equality by attribute names and values, and a representation. Warning: Namespace instances do not have direct access to the dict methods. But since it is a dict object, it is possible to call these methods the following way: dict.get(ns, 'key') See http://code.activestate.com/recipes/577887-a-simple-namespace-class/ """ def __init__(self, obj=None): dict.__init__(self, obj if obj else {}) def __dir__(self): return list(self) def __repr__(self): return "%s(%s)" % (type(self).__name__, dict.__repr__(self)) def __getattribute__(self, name): try: return self[name] except KeyError: msg = "'%s' object has no attribute '%s'" raise AttributeError(msg % (type(self).__name__, name)) def __setattr__(self, name, value): self[name] = value def __delattr__(self, name): del self[name] class Section(object): READERS = {bool: ConfigParser.SafeConfigParser.getboolean, float: ConfigParser.SafeConfigParser.getfloat, int: ConfigParser.SafeConfigParser.getint, str: ConfigParser.SafeConfigParser.get} def __init__(self, parser, group): self._parser = parser self._group = group self._schema = None self._parsed = {} def matches(self, name): return self._group == name.lower() def is_collection(self): return False def parse(self, section): if self._schema is None: return None for option, default, filter_ in self._schema: try: if filter_ is None: reader = ConfigParser.SafeConfigParser.get filter_ = lambda x: x else: reader = Section.READERS[type(default)] self._parsed[option] = filter_(reader(self._parser, section, option)) # Remove option once handled (see the check function). self._parser.remove_option(section, option) except ConfigParser.Error: if filter_ is None: self._parsed[option] = default else: self._parsed[option] = filter_(default) return Namespace(self._parsed) @property def group(self): return self._group def _parse_verify(self, value): """if value is either 'True' or 'False' it returns that value as a bool, otherwise it returns the value""" value = value.strip().lower() if value == 'true': return True elif value == 'false': return False else: return os.path.expanduser(value) def _parse_write_support(self, value): """returns True if value is YesPlease..., this is a rather dirty solution, but it works fine (TM)""" value = value.strip() if value == 'YesPleaseIDoHaveABackupOfMyData': return True else: return False class AccountSection(Section): def __init__(self, parser): Section.__init__(self, parser, 'accounts') self._schema = [ ('user', '', None), ('passwd', '', None), ('resource', '', None), ('auth', 'basic', None), ('verify', 'true', self._parse_verify), ('write_support', '', self._parse_write_support), ] def is_collection(self): return True def matches(self, name): match = re.match('account (?P.*)', name, re.I) if match: self._parsed['name'] = match.group('name') return match is not None class SQLiteSection(Section): def __init__(self, parser): Section.__init__(self, parser, 'sqlite') self._schema = [ ('path', ConfigurationParser.DEFAULT_DB_PATH, os.path.expanduser), ] class ConfigurationParser(object): """A Configuration setup tool. This object takes care of command line parsing as well as configuration loading. It also prepares logging and updates its output level using the debug flag read from the command-line or the configuration file. """ DEFAULT_DB_PATH = xdg.BaseDirectory.save_data_path('pycard') + '/abook.db' DEFAULT_PATH = "pycard" DEFAULT_FILE = "pycard.conf" def __init__(self, desc, check_accounts=True): # Set the configuration current schema. self._sections = [AccountSection, SQLiteSection] # Build parsers and set common options. self._check_accounts = check_accounts self._conf_parser = ConfigParser.SafeConfigParser() self._arg_parser = argparse.ArgumentParser(description=desc) self._arg_parser.add_argument( "-v", "--version", action="version", version=__version__) self._arg_parser.add_argument( "-c", "--config", action="store", dest="filename", default=self._get_default_configuration_file(), metavar="FILE", help="an alternate configuration file") self._arg_parser.add_argument( "--debug", action="store_true", dest="debug", help="enables debugging") def parse(self): """Start parsing. Once the commandline parser is eventually configured with specific options, this function must be called to start parsing. It first parses the command line, and then the configuration file. If parsing is successful, the function check is then called. When check is a success, the Configuration instance is returned. On any error, None is returned. """ args = self._read_command_line() # Prepare the logger with the level read from command line. logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) if not args.filename: logging.error('Could not find configuration file') return None try: if not self._conf_parser.read(os.path.expanduser(args.filename)): logging.error('Cannot read %s', args.filename) return None else: logging.debug('Using configuration from %s', args.filename) except ConfigParser.Error, e: logging.error("Could not parse %s: %s", args.filename, e) return None conf = self._read_configuration(args) # Update the logger using the definitive output level. logging.getLogger().setLevel(logging.DEBUG if conf.debug else logging.INFO) return conf if self.check(conf) else None def check(self, ns): """Check the configuration before returning it from parsing. This default implementation warns the user of the remaining options found in the configuration file. It then checks the validity of the common configuration values. It returns True on success, False otherwise. This function can be overriden to augment the checks or the configuration tweaks achieved before the parsing function returns. """ result = True for section in self._conf_parser.sections(): for option in self._conf_parser.options(section): logging.debug("Ignoring %s:%s in configuration file", section, option) if self._check_accounts: if self.check_property(ns, 'accounts'): for account in ns.accounts: result &= self.check_account(account) else: logging.error("No account found") result = False return result def check_account(self, ns): result = True if not ns.auth in ['basic', 'digest']: logging.error("Value %s is not allowed for in Account %s:auth", ns.auth, ns.name) result = False if not self.check_property(ns, 'resource', 'Account %s:resource' % ns.name): return False if not len(ns.passwd): hostname = urlsplit(ns.resource).hostname try: auths = netrc().authenticators(hostname) except IOError: auths = False if auths: if not ns.user or auths[0] == ns.user: logging.debug("Read password for user %s on %s in .netrc", auths[0], hostname) ns.user = auths[0] ns.passwd = auths[2] else: logging.error("User %s not found for %s in .netrc", ns.user, hostname) result = False elif ns.user: try: import keyring except ImportError: pass else: ns.passwd = keyring.get_password('pycarddav:'+ns.name, ns.user) # Do not ask for password if execution is already doomed. if result and not ns.passwd: prompt = 'CardDAV password (account ' + ns.name + '): ' ns.passwd = getpass.getpass(prompt=prompt) else: logging.error("Missing credentials for %s", hostname) result = False return result def check_property(self, ns, property_, display_name=None): names = property_.split('.') obj = ns try: for name in names: obj = dict.get(obj, name) if not obj: raise AttributeError() except AttributeError: logging.error('Mandatory option %s is missing', display_name if display_name else property_) return False return True def dump(self, conf, intro='Using configuration:', tab=1): """Dump the loaded configuration using the logging framework. The values displayed here are the exact values which are seen by the program, and not the raw values as they are read in the configuration file. """ logging.debug(intro) for name, value in sorted(dict.copy(conf).iteritems()): if type(value) is list and not isinstance(value[0], basestring): for o in value: self.dump(o, '\t' * tab + name + ':', tab + 1) elif type(value) is Namespace: self.dump(value, '\t' * tab + name + ':', tab + 1) elif name != 'passwd': logging.debug('%s%s: %s', '\t'*tab, name, value) def _read_command_line(self): items = {} for key, value in vars(self._arg_parser.parse_args()).iteritems(): if '__' in key: section, option = key.split('__') items.setdefault(section, Namespace({}))[option] = value else: items[key] = value return Namespace(items) def _read_configuration(self, overrides): """Build the configuration holder. First, data declared in the configuration schema are extracted from the configuration file, with type checking and possibly through a filter. Then these data are completed or overriden using the values read from the command line. """ items = {} try: if self._conf_parser.getboolean('default', 'debug'): overrides['debug'] = True except ValueError: pass for section in self._conf_parser.sections(): parser = self._get_section_parser(section) if not parser is None: values = parser.parse(section) if parser.is_collection(): if not items.has_key(parser.group): items[parser.group] = [] items[parser.group].append(values) else: items[parser.group] = values for key in dir(overrides): items[key] = Namespace.get(overrides, key) return Namespace(items) def _get_section_parser(self, section): for cls in self._sections: parser = cls(self._conf_parser) if parser.matches(section): return parser return None def _get_default_configuration_file(self): """Return the configuration filename. This function builds the list of paths known by pycarddav and then return the first one which exists. The first paths searched are the ones described in the XDG Base Directory Standard. Each one of this path ends with DEFAULT_PATH/DEFAULT_FILE. On failure, the path DEFAULT_PATH/DEFAULT_FILE, prefixed with a dot, is searched in the home user directory. Ultimately, DEFAULT_FILE is searched in the current directory. """ paths = [] resource = os.path.join( ConfigurationParser.DEFAULT_PATH, ConfigurationParser.DEFAULT_FILE) paths.extend([os.path.join(path, resource) for path in xdg.BaseDirectory.xdg_config_dirs]) paths.append(os.path.expanduser(os.path.join('~', '.' + resource))) paths.append(os.path.expanduser(ConfigurationParser.DEFAULT_FILE)) for path in paths: if os.path.exists(path): return path return None class SyncConfigurationParser(ConfigurationParser): """A specialized setup tool for synchronization.""" def __init__(self): ConfigurationParser.__init__(self, "syncs the local db to the CardDAV server") self._arg_parser.add_argument( "-a", "--account", action="append", dest="sync__accounts", metavar="NAME", help="use only the NAME account (can be used more than once)") def check(self, ns): result = ConfigurationParser.check(self, ns) accounts = [account.name for account in ns.accounts] if ns.sync.accounts: for name in set(ns.sync.accounts): if not name in [a.name for a in ns.accounts]: logging.warn('Unknown account %s', name) ns.sync.accounts.remove(name) if len(ns.sync.accounts) == 0: logging.error('No valid account selected') result = False else: ns.sync.accounts = accounts for account in ns.accounts: if account.resource[-1] != '/': account.resource = account.resource + '/' ns.sync.accounts = set(ns.sync.accounts) return result pyCardDAV-0.6.1/NEWS.txt0000664000175000017500000001017612245117275013631 0ustar cgcg00000000000000News ==== 26.11.2013: pyCardDAV v0.6.1 released The man pages should now be included in the release tarball. 25.11.2013: pyCardDAV v0.6 released This is mostly a bug fix release (thanks to Jamie McClelland for fixing two bugs), but also introduces keyring support (thanks to Steven Allen). Have a look at the README for further information on keyring support. 02.09.2013: pyCardDAV v.0.5.1 released pyCardDAV v0.5.1 is released. This is a bugfix release, if everything works fine for you, there is no need to upgrade. Database deletion should not be necessary. 15.06.2013: pyCardDAV v.0.5.0 released **New** This release brings support for multiple CardDAV accounts. See the usage instructions, the supplied example config and/or pc_query --help. Also support for more CardDAV servers is included. If you are upgrading, you need to delete the local database, otherwise pyCardDAV will refuse to work. **Attention** In accordance with RFC 6352 all VCards that are imported or changed by pyCadDAV will automatically get a random UID (if they haven't one already), as some CardDAV servers, e.g. Owncloud require these. 28.03.2013: pyCardDAV v0.4.1 released pyCardDAV v0.4.1 is released. This is a bugfix release, if everything works fine for you, there is no need to upgrade. Database deletion should not be necessary. 15.11.2012: pyCardDAV v0.4 released pyCardDAV v0.4 is released. This is a mayor rewrite (again), so some previously fixed bugs might be back in. If your upgrading, you should delete your database file first. On the plus side, there are some new features in pyCardDAV: experimental write support in the backend import & export vCards import addresses directly from mutt speed increase in (initial) sync due to switching from pycurl to requests detects removed cards on server and delete them locally can delete cards locally and then on server Also the license has changed to MIT/Expat, see the COPYING file for details (but a beer is still appreciated). PyCurl is not required anymore, pyCardDAV relies on requests now (which needs to be installed). Special thanks to David Soulayrol who made a lot of this happen. Attention: please make sure you have a backup when you enable write support, see Usage for more details. 27.01.2012: pyCardDAV v0.3.3_ released: **New** sabredav/owncloud support, thanks Davide Gerhard. Fixes a bug where properties with no type parameters were not printed. This release also fixes a small database bug. The config file has a new entry (*davserver*) which you can set to either davical or sabredav (depending on your CardDAV server). Future: The source code has been cleaned up quite a bit (nearly every line of code has been touched) and some features have been added. Write support is nearly finished in the backend (but will probably not be included in the next release yet), but the frontend is still really buggy and a pain to use. If you want to have a look, check the repository out at github and check the branch *write_support* (but it might me broken). 06.01.2012: pyCardDAV v0.3.2_ released: this is a minor bugfix update, db deleting should not be necessary. If everything is working fine at the moment, there is no need to upgrade. 06.01.2012: pyCardDAV v0.3.1_ released: this bugfix release fixes some bugs on Debian and a formatting bug (thanks to Antoine Sirinelli) and one more unicode bug (thanks to Thomas Klausner). Also, some more meaningful error messages were added. **Attention** if you are upgrading: you should delete the old database again and resync using pycardsyncer 08.12.2011: pyCardDAV v0.3_ released: this fixes an unicode bug and has a lot of internal changes **Attention** pc-query has been renamed to pc_query, make sure to delete the old database, also the config file format has somewhat changed 10.10.2011: pyCardDAV v0.2.1_ released this fixes a minor bug in the example config file 14.09.2011: pyCardDAV v0.2_ released **New** config files are now supported 13.09.2011: pyCardDAV moved to github_ feel free to fork etc. 12.08.2011: pyCardDAV v0.1_ released first public version pyCardDAV-0.6.1/COPYING0000664000175000017500000000213612245116621013336 0ustar cgcg00000000000000# Copyright (c) 2011-2012 Christian Geier, David Soulayrol # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pyCardDAV-0.6.1/bin/0000775000175000017500000000000012245117351013052 5ustar cgcg00000000000000pyCardDAV-0.6.1/bin/pycardsyncer0000775000175000017500000000405312245116621015507 0ustar cgcg00000000000000#!/usr/bin/env python2 # vim: set ts=4 sw=4 expandtab sts=4: # Copyright (c) 2011-2013 Christian Geier & contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. try: import logging import sys from pycarddav import SyncConfigurationParser from pycarddav import controllers from pycarddav import capture_user_interruption except ImportError as error: sys.stderr.write(str(error)) sys.exit(1) capture_user_interruption() # Read configuration. conf_parser = SyncConfigurationParser() conf = conf_parser.parse() if conf is None: sys.exit(1) if conf.debug: conf_parser.dump(conf) rvalue = 0 for one in conf.accounts: if one.name in conf.sync.accounts: logging.debug('start syncing account {0}'.format(one.name)) conf.account = one try: controllers.sync(conf) except Exception as error: if conf.debug: raise logging.critical('While syncing account "{0}" an error occured:\n '.format(one.name) + str(error)) rvalue = 1 sys.exit(rvalue) pyCardDAV-0.6.1/bin/pycard-import0000775000175000017500000001367512245116621015605 0ustar cgcg00000000000000#!/usr/bin/env python2 # -*- coding: utf-8 -*- # vim: set ts=4 sw=4 expandtab sts=4: # Copyright (c) 2011-2013 Christian Geier & contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """A pyCardDAV tool to create VCards from one email. """ import email import email.header import os import sys import traceback import logging import pycarddav import pycarddav.backend import pycarddav.model import pycarddav.ui class ImportConfigurationParser(pycarddav.ConfigurationParser): """A specialized setup tool for importing a contact.""" def __init__(self): pycarddav.ConfigurationParser.__init__(self, 'Import contacts from a mail on input.', check_accounts=False) self._arg_parser.epilog = 'Only the From header is parsed if no header is specified.' self._arg_parser.add_argument( '--batch', action='store_true', dest='batch', default=False, help='do not open the editor') self._arg_parser.add_argument( '-n', '--dry-run', action='store_true', dest='dry_run', default=False, help='do not actually update the database (implies --batch)') self._arg_parser.add_argument( "-a", "--account", action="store", dest="sync__account", metavar="NAME", help="add to NAME account (can be used only once), if no valid acconut name is given, the first one from the config will be used") # Headers selection. To: is the default. self._arg_parser.add_argument( '-f', '--from', action='append_const', dest='headers', const='From', help='import the content of the From header') self._arg_parser.add_argument( '-t', '--to', action='append_const', dest='headers', const='To', help='import the content of the To header') self._arg_parser.add_argument( '--cc', action='append_const', dest='headers', const='Cc', help='import the content of the Cc header') self._arg_parser.add_argument( '--bcc', action='append_const', dest='headers', const='Bcc', help='import the content of the Bcc header') def check(self, conf): accounts = [account.name for account in conf.accounts] if conf.sync.account: if conf.sync.account not in accounts: logging.critical(conf.sync.account + ' is not a valid account') sys.exit(1) else: conf.sync.account = accounts[0] if conf.dry_run: conf.batch = True if not conf.headers: conf.headers = ['From'] return pycarddav.ConfigurationParser.check(self, conf) class MailParser(object): def __init__(self, conf, pipe): self._conf = conf self._db = pycarddav.backend.SQLiteDb( conf.sqlite.path, "utf-8", "stricts", False) self._msg = email.message_from_string(pipe.read()) def process_addresses(self, headers): for header in headers: address, display_name = self.parse_address(self._msg[header]) if address is None: continue vcard = pycarddav.model.vcard_from_email(display_name, address) if self._conf.batch: if not self._conf.dry_run: self._db.update(vcard, self._conf.sync.account, vcard.href, status=pycarddav.backend.NEW) else: pycarddav.ui.start_pane(pycarddav.ui.EditorPane(self._db, self._conf.sync.account, vcard)) def parse_address(self, header): if header is None: return None, '' address_string = [] addresses = email.header.decode_header(header) for string, enc in addresses: try: string = string.decode(enc) except TypeError: string = unicode(string) address_string.append(string) address_string = ' '.join(address_string) display_name, address = email.utils.parseaddr(address_string) return address, display_name def capture_tty(): """Walk the parent processes until a TTY is found. """ sys.stdin = open('/dev/tty') sys.stdout = open('/dev/tty', 'wb') sys.stderr = open('/dev/tty', 'wb') os.dup2(sys.stdin.fileno(), 0) os.dup2(sys.stdout.fileno(), 1) os.dup2(sys.stderr.fileno(), 2) def release_tty(): """closing the files""" sys.stdin.close() sys.stdout.close() sys.stderr.close() def do_import(): """does the real work""" conf = ImportConfigurationParser().parse() if conf is None: sys.exit(1) parser = MailParser(conf, sys.stdin) if not conf.batch: capture_tty() try: parser.process_addresses(conf.headers) except Exception: exc_type, exc_value, exc_tb = sys.exc_info() traceback.print_exception(exc_type, exc_value, exc_tb, file=sys.stdout) finally: if not conf.batch: release_tty() if __name__ == '__main__': do_import() pyCardDAV-0.6.1/bin/pc_query0000775000175000017500000001113212245116621014624 0ustar cgcg00000000000000#!/usr/bin/env python2 # vim: set ts=4 sw=4 expandtab sts=4: # Copyright (c) 2011-2013 Christian Geier & contributors # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. try: import argparse import logging import sys from pycarddav import ConfigurationParser from pycarddav import controllers from pycarddav import capture_user_interruption except ImportError as error: sys.stderr.write(str(error)) sys.exit(1) class QueryConfigurationParser(ConfigurationParser): """A specialized setup tool for cards query.""" def __init__(self): ConfigurationParser.__init__(self, "prints contacts cards matching a search string", check_accounts=False) self._arg_parser.add_argument( "-A", action="store_true", dest="query__display_all", default=False, help="prints the whole card, not only name, " "telephone numbers and email addresses") self._arg_parser.add_argument( "-m", action="store_true", dest="query__mutt_format", default=False, help="only prints email addresses, in a mutt friendly format") self._arg_parser.add_argument( "-t", action="store_true", dest="query__tel", default=False, help="only prints telephone number, analogue to -m " "(but in different sequence)") # self._arg_parser.add_argument( # "-e", action="store_true", dest="query__edit", default=False, # help="edit the contact file.\n" # "NOTE: this feature is experimental and will probably corrupt " # "your *local* database. Your remote CardDAV resource will stay " # "untouched, as long as You don't enable write support for the " # "syncer.") self._arg_parser.add_argument( "query__search_string", metavar="SEARCHSTRING", default="", help="the string to search for", nargs="?") self._arg_parser.add_argument( "-b", "--backup", action="store", dest="query__backup", metavar="BACKUP", help="backup the local db to BACKUP, " "if a SEARCHSTRING is present, only backup cards matching it.") self._arg_parser.add_argument( "-i", "--import", metavar="FILE", type=argparse.FileType("r"), dest="query__importing", help="import vcard from FILE or STDIN into the first specified account") self._arg_parser.add_argument( "--delete", dest="query__delete", action="store_true", help="delete card matching SEARCHSTRING") self._arg_parser.add_argument( "-a", "--account", action="append", dest="sync__accounts", metavar="NAME", help="use only the NAME account (can be used more than once)") def check(self, ns): result = ConfigurationParser.check(self, ns) accounts = [account.name for account in ns.accounts] if ns.sync.accounts: for name in set(ns.sync.accounts): if not name in [a.name for a in ns.accounts]: logging.warn('Uknown account %s', name) ns.sync.accounts.remove(name) if len(ns.sync.accounts) == 0: logging.error('No valid account selected') result = False else: ns.sync.accounts = accounts ns.sync.accounts = list(set(ns.sync.accounts)) return result capture_user_interruption() # Read configuration. conf_parser = QueryConfigurationParser() conf = conf_parser.parse() if conf is None: sys.exit(1) if conf.debug: conf_parser.dump(conf) controllers.query(conf) pyCardDAV-0.6.1/setup.cfg0000664000175000017500000000007312245117351014123 0ustar cgcg00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 pyCardDAV-0.6.1/MANIFEST.in0000664000175000017500000000023712245117066014045 0ustar cgcg00000000000000include pycard.conf.sample include README.rst include CONTRIBUTING.txt include CONTRIBUTORS.txt include COPYING include NEWS.txt recursive-include doc/man *.1 pyCardDAV-0.6.1/CONTRIBUTORS.txt0000664000175000017500000000076712245116621015011 0ustar cgcg00000000000000Christian Geier David Soulayrol - david.soulayrol [at] gmail [dot] com - http://david.soulayrol.name Aurélien Gâteau - http://agateau.com Hugo Osvaldo Barrera Ben Boeckel - mathstuf [at] gmail [dot] com Thomas Glanzmann - thomas@glanzmann.de - http://thomas.glanzmann.de Johannes Goetzfried - johannes@jgoetzfried.de - http://jgoetzfried.de Steven Allen - http://stebalien.com Jamie McClelland - jm@mayfirstorg - http://current.workingdirectory.net pyCardDAV-0.6.1/doc/0000775000175000017500000000000012245117351013047 5ustar cgcg00000000000000pyCardDAV-0.6.1/doc/man/0000775000175000017500000000000012245117351013622 5ustar cgcg00000000000000pyCardDAV-0.6.1/doc/man/pc_query.10000664000175000017500000000760412245116621015541 0ustar cgcg00000000000000'\" t .\" Title: pc_query .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets v1.76.1 .\" Date: 11/25/2013 .\" Manual: \ \& .\" Source: \ \& .\" Language: English .\" .TH "PC_QUERY" "1" "11/25/2013" "\ \&" "\ \&" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .\" http://bugs.debian.org/507673 .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" ----------------------------------------------------------------- .\" * set default formatting .\" ----------------------------------------------------------------- .\" disable hyphenation .nh .\" disable justification (adjust text to left margin only) .ad l .\" ----------------------------------------------------------------- .\" * MAIN CONTENT STARTS HERE * .\" ----------------------------------------------------------------- .SH "NAME" pc_query \- query the local address book .SH "SYNOPSIS" .sp \fBpc_query\fR [\fIOPTIONS\fR] [SEARCHSTRING] .SH "DESCRIPTION" .sp pc_query(1) prints contacts from a local address book, synchronized with remote CardDAV resources with pycardsyncer(1)\&. Contact information is kept in the \fIvCard\fR format, one set of information, corresponding to a business card, is therefore often referred to as a \fIcard\fR\&. pc_query(1) can also import and export vcards from/to the database\&. pc_query(1) is part of the \fIpycarddav\fR package\&. .SH "OPTIONS" .SS "BASIC STARTUP OPTIONS" .PP \fB\-a, \-\-account\fR=\fINAME\fR .RS 4 Only use the account/resource \fINAME\fR (this option can be used more than once)\&. .RE .PP \fB\-c, \-\-config\fR=\fICONFIG\fR .RS 4 Use the configuration file \fICONFIG\fR, otherwise pc_query(1) will look in \fI$HOME/\&.pycard/\fR and \fI$HOME/\&.config/pycard/\fR for files named \fIpycard\&.conf\fR\&. .RE .PP \fB\-\-debug\fR .RS 4 This option enables debugging output\&. .RE .PP \fB\-h, \-\-help\fR .RS 4 Print a small help text and exit\&. .RE .PP \fB\-v, \-\-version\fR .RS 4 Print pc_query\(cqs version number and exit\&. .RE .SS "PRINTING OPTIONS" .sp These options can only be used with \fISEARCHSTRING\fR and determine the output format of pc_query(1)\&. Without any options pc_query(1) will print the name, telephone numbers and email addresses of all matching contact cards\&. .PP \fB\-A\fR .RS 4 Prints the whole card\&. .RE .PP \fB\-m\fR .RS 4 Only prints names and email addresses, in a mutt friendly format (one line per email address)\&. .RE .PP \fB\-t\fR .RS 4 Only prints names and telephone numbers, analogue to \-m (but in different sequence) .RE .SS "FILE INPUT AND OUTPUT OPTIONS" .PP \fB\-b, \-\-backup\fR\fI=\*(AqFILE\fR .RS 4 Backup the local db to BACKUP, if a SEARCHSTRING is present, only backup cards matching it\&. .RE .PP \fB\-\-delete\fR .RS 4 Delete card matching \fISEARCHSTRING\fR, if more than one matches, the user has to choose one card in an interactive user interface\&. .RE .PP \fB\-i, \-\-import\fR=\fIFILE\fR .RS 4 Import vCard from FILE or \fISTDIN\fR into the first specified (or default) account .RE .SH "AUTHOR" .sp pc_query was mostly written by Christian Geier, with a lot of help by others, see \fICONTRIBUTORS\&.txt\fR in the pycarddav distribution\&. .SH "RESOURCES" .sp Main web site: http://lostpackets\&.de/pycarddav/ Please report bugs via the contact information at the above web site or via github: http://github\&.com/geier/khal/\&. .SH "SEE ALSO" .sp pycardsyncer(1), pycard\-import(1) .SH "COPYING" .sp Copyright (C) 2011\-2013 Christian Geier and Contributors\&. pc_query and pycarddav are released under the terms of the Expat/MIT license, see the \fICOPYING\fR file distributed with pycarddav\&. pyCardDAV-0.6.1/doc/man/pycardsyncer.10000664000175000017500000000516612245116621016421 0ustar cgcg00000000000000'\" t .\" Title: pycardsyncer .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets v1.76.1 .\" Date: 11/25/2013 .\" Manual: \ \& .\" Source: \ \& .\" Language: English .\" .TH "PYCARDSYNCER" "1" "11/25/2013" "\ \&" "\ \&" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .\" http://bugs.debian.org/507673 .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" ----------------------------------------------------------------- .\" * set default formatting .\" ----------------------------------------------------------------- .\" disable hyphenation .nh .\" disable justification (adjust text to left margin only) .ad l .\" ----------------------------------------------------------------- .\" * MAIN CONTENT STARTS HERE * .\" ----------------------------------------------------------------- .SH "NAME" pycardsyncer \- synchronizes CardDAV resources to local db .SH "SYNOPSIS" .sp \fBpycardsyncer\fR [\fIOPTIONS\fR] .SH "DESCRIPTION" .sp pycardsyncer(1) synchronizes remote CardDAV resources to a local database that can then be queried and modified with pc_query(1)\&. .SH "OPTIONS" .PP \fB\-a, \-\-account\fR=\fINAME\fR .RS 4 Sync only the account/resource \fINAME\fR (this option can be used more than once)\&. .RE .PP \fB\-c, \-\-config\fR=\fICONFIG\fR .RS 4 Use the configuration file \fICONFIG\fR, otherwise pycardsyncer will look in \fI$HOME/\&.pycard/\fR and \fI$HOME/\&.config/pycard/\fR for files named \fIpycard\&.conf\fR\&. .RE .PP \fB\-\-debug\fR .RS 4 This option enables debugging output\&. .RE .PP \fB\-h, \-\-help\fR .RS 4 Print a small help text and exit\&. .RE .PP \fB\-v, \-\-version\fR .RS 4 Print pycardsyncer\(cqs version number and exit\&. .RE .SH "AUTHOR" .sp pycardsyncer was mostly written by Christian Geier, with a lot of help by others, see \fICONTRIBUTORS\&.txt\fR in the pycarddav distribution\&. .SH "RESOURCES" .sp Main web site: http://lostpackets\&.de/pycarddav/ Please report bugs via the contact information at the above web site or via github: http://github\&.com/geier/khal/\&. .SH "SEE ALSO" .sp pc_query(1), pycard\-import(1) .SH "COPYING" .sp Copyright (C) 2011\-2013 Christian Geier and Contributors\&. pycardsyncer and pycarddav are released under the terms of the Expat/MIT license, see the \fICOPYING\fR file distributed with pycarddav\&. pyCardDAV-0.6.1/doc/man/pycard-import.10000664000175000017500000000630212245116621016476 0ustar cgcg00000000000000'\" t .\" Title: pycard-import .\" Author: [see the "AUTHOR" section] .\" Generator: DocBook XSL Stylesheets v1.76.1 .\" Date: 11/25/2013 .\" Manual: \ \& .\" Source: \ \& .\" Language: English .\" .TH "PYCARD\-IMPORT" "1" "11/25/2013" "\ \&" "\ \&" .\" ----------------------------------------------------------------- .\" * Define some portability stuff .\" ----------------------------------------------------------------- .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .\" http://bugs.debian.org/507673 .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" ----------------------------------------------------------------- .\" * set default formatting .\" ----------------------------------------------------------------- .\" disable hyphenation .nh .\" disable justification (adjust text to left margin only) .ad l .\" ----------------------------------------------------------------- .\" * MAIN CONTENT STARTS HERE * .\" ----------------------------------------------------------------- .SH "NAME" pycard-import \- import email addresses from mails from stdin .SH "SYNOPSIS" .sp \fBpycard\-import\fR [\fIOPTIONS\fR] .SH "DESCRIPTION" .sp pycard\-import(1) imports email addresses from mails piped into its stdin and saves those addresses to the database used by pycardsyncer(1) and pc_query(1)\&. pycard\-import(1) is part of the \fIpycarddav\fR package\&. .SH "OPTIONS" .SS "BASIC STARTUP OPTIONS" .PP \fB\-a, \-\-account\fR=\fINAME\fR .RS 4 Only use the account/resource \fINAME\fR (this option can be used more than once)\&. .RE .PP \fB\-c, \-\-config\fR=\fICONFIG\fR .RS 4 Use the configuration file \fICONFIG\fR, otherwise pycard\-import(1) will look in \fI$HOME/\&.pycard/\fR and \fI$HOME/\&.config/pycard/\fR for files named \fIpycard\&.conf\fR\&. .RE .PP \fB\-\-debug\fR .RS 4 This option enables debugging output\&. .RE .PP \fB\-h, \-\-help\fR .RS 4 Print a small help text and exit\&. .RE .PP \fB\-v, \-\-version\fR .RS 4 Print pycard\-import\(cqs version number and exit\&. .RE .SS "IMPORTING OPTIONS" .PP \fB\-\-batch\fR .RS 4 Do not open the editor\&. .RE .PP \fB\-\-bcc\fR .RS 4 Import the content of the Bcc header\&. .RE .PP \fB\-\-cc\fR .RS 4 Import the content of the Cc header\&. .RE .PP \fB\-f, \-\-from\fR .RS 4 Import the content of the From header\&. .RE .PP \fB\-n, \-\-dry\-run\fR .RS 4 Do not actually update the database (implies \-\-batch)\&. .RE .PP \fB\-t, \-\-to\fR .RS 4 Import the content of the To header\&. .RE .SH "AUTHOR" .sp pycard\-import was mostly written by Christian Geier, with a lot of help by others, see \fICONTRIBUTORS\&.txt\fR in the pycarddav distribution\&. .SH "RESOURCES" .sp Main web site: http://lostpackets\&.de/pycarddav/ Please report bugs via the contact information at the above web site or via github: http://github\&.com/geier/khal/\&. .SH "SEE ALSO" .sp pycardsyncer(1), pc_query(1) .SH "COPYING" .sp Copyright (C) 2011\-2013 Christian Geier and Contributors\&. pycard\-import and pycarddav are released under the terms of the Expat/MIT license, see the \fICOPYING\fR file distributed with pycarddav\&. pyCardDAV-0.6.1/PKG-INFO0000664000175000017500000002374212245117351013407 0ustar cgcg00000000000000Metadata-Version: 1.1 Name: pyCardDAV Version: 0.6.1 Summary: A CardDAV based address book tool Home-page: http://lostpackets.de/pycarddav/ Author: Christian Geier Author-email: pycarddav@lostpackets.de License: Expat/MIT Description: About ===== *pyCardDAV* is a simple to use CardDAV_ CLI client. It has built in support for mutt's *query_command* but also works very well solo. *pyCardDAV* consists of *pycardsyncer*, a program for syncing your CardDAV resource into a local database and of *pc_query*, a program for querying the local database. *pyCardDAV* is some ugly python_ code (actually, it's not *that* bad anymore…) that holds together vobject_, lxml_, requests_ and pysqlite_. .. _CardDAV: http://en.wikipedia.org/wiki/CardDAV .. _python: http://python.org/ .. _vobject: http://vobject.skyhouseconsulting.com/ .. _lxml: http://lxml.de/ .. _pysqlite: http://code.google.com/p/pysqlite/ .. _requests: http://python-requests.org Features -------- (or rather: limitations) - *pyCardDAV* can only use one address book resource at the moment - *pyCardDAV* is only tested against davical, owncloud and sabredav - *pyCardDAV* can import the sender's address directly from mutt - *pyCardDAV* can backup and import to/from .vcf files - *pyCardDAV* can add email addresses directly from mutt - *pyCardDAV* only understands VCard 3.0 - *pyCardDAV* is not python 3 compatible yet Feedback -------- Please do provide feedback if *pyCardDAV* works for you or even more importantly if it doesn't. You can reach me by email at pycarddav (at) lostpackets (dot) de , by jabber/XMPP at geier (at) jabber (dot) ccc (dot) de or via github_ .. _github: https://github.com/geier/pycarddav/ Installation ------------ You can download *pyCardDAV* either from the above download link or check it out from git (at github). Then install *pyCardDAV* by executing *python setup.py install*. If you feel more adventurous you can always the *develop* branch on github, which *should* always be in a usable state. pyCardDAV is also available on pypi_ and can be installed via pip install pycarddav or easy_install pycarddav. Copy and edit the supplied pycard.conf.sample file (default location is ~/.config/pycard/pycard.conf). If you don't want to store the password in clear text in the config file, *pyCardDAV* will ask for it while syncing. Make sure you have sqlite3 (normally available by default), vobject, lxml(>2), requests (>0.10), urwid (>0.9) pyxdg, installed. Users of python 2.6 will also need to install argparse. *pyCardDAV* has so far been successfully tested on recent versions of FreeBSD, NetBSD, Debian and Ubuntu with python 2.6 and 2.7 and against davical 0.9.9.4 - 1.0.1 (later versions should be ok, too, but 0.9.9.3 and earlier don't seem to work), owncloud and sabredav. .. _pypi: https://pypi.python.org/pypi/pyCardDAV/ .. _git: http://github.com/geier/pycarddav/ Usage ----- *pyCardDAV* consists of three scripts, *pycardsyncer* which is used to sync the local database with the server, *pc_query* to interact with the local database and *pycard-import* to import email addresses from mutt. Execute pycardsyncer to sync your addresses to the local database. You can test pc_query with:: % pc_query searchstring By default *pyCardDAV* only prints the names, email addresses and telephone numbers of contacts matching the search string, to see all vCard properties use the "-A" option. For usage with mutt etc., *pyCardDAV* can also print only email addresses in a mutt friendly format (with the "-m" option). Edit your mutt configuration so that query_command uses pc_query: Example from .muttrc:: set query_command="/home/username/bin/pc_query -m '%s'" The current version features experimental write support. If you want to test this, first make sure **you have a backup of your data** (but please do *NOT* rely on *pc_query --backup* for this just yet), then you can put the line:: write_support = YesPleaseIDoHaveABackupOfMyData in your config file (needs to be put into each *Account* section you want to enable write support for). You can also import, delete or backup single cards (backup also works for the whole collection, but please don't rely on it just yet). See *pc_query --help* for how to use these and for some more options. *pycarddav* can be configured to use different CardDAV accounts, see the example config for details. An account can be specified with *-a account_name* with all three utilies. If no account is chosen all searching and syncing actions will use all configured accounts, while on adding cards the first configured account will be used. Keyring support --------------- *pycarddav* supports keyring_, (version >=3.0). To use it, you need to add a password to the keyring via:: keyring set pycarddav:$account $username where $account is the name of an account as configured in your configuration file and $username is the corresponding username (and then have no password configured for that account). For more details on configuring keyring have a look at its documentation_. .. _keyring: https://pypi.python.org/pypi/keyring .. _documentation: https://pypi.python.org/pypi/keyring Import Addresses from Mutt -------------------------- You can directly add sender addresses from mutt to *pyCardDAV*, either adding them to existing contacts or creating a new one. If write support is enabled, they will be uploaded on the server during the next sync. Example from .muttrc:: macro index,pager A "pycard-import" "add sender address to pycardsyncer" SSL --- If you use SSL to interact with your CardDAV Server (you probably should) and you don't have a certificate signed by a CA your OS Vendor trusts (like a self-signed certificate or one signed by CAcert) you can set *verify* to a path to the CA's root file (must be in pem format). If you don't want any certificate checking set *verify* to *false* to disable *any* ssl certificate checking (this is not recommended). Conflict Resolution ------------------- In case of conflicting edits (local VCard changed while remote VCard also changed), are "resolved" by pycarddav through overwriting the local VCard with the remote one (meaning local edits are lost in this case). Syncing more frequently can prevent this. Additional Information ---------------------- For now, VCard properties that have no value are not shown. Also, you should be able to use *pyCardDAV*'s CardDAV implementation for other projects. See the *CardDAV* class in *pycarddav/carddav.py*. In accordance with RFC 6352 all VCards that are imported or changed by pyCadDAV will automatically get a random UID (if they haven't one already), as some CardDAV servers, e.g. Owncloud require these. Debian Wheezy Quickstart ------------------------ apt-get install python-requests python-vobject python-pytest python-urwid python-lxml python-pyxdg sudo python setup.py install mkdir -p ~/.config/pycard chmod 700 ~/.config/pycard cp pycard.conf.sample ~/.config/pycard/pycard.conf License ------- *pyCardDAV* is released under the Expat/MIT License: Copyright (c) 2011-2013 Christian Geier and contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: License :: OSI Approved :: MIT License Classifier: Environment :: Console :: Curses Classifier: Intended Audience :: End Users/Desktop Classifier: Operating System :: POSIX Classifier: Programming Language :: Python :: 2 :: Only Classifier: Topic :: Utilities Classifier: Topic :: Communications :: Email :: Address Book Requires: lxml Requires: vobject Requires: requests Requires: urwid Requires: pyxdg