├── .gitignore ├── .travis.yml ├── CHANGELOG ├── CONTRIBUTING.txt ├── CONTRIBUTORS.txt ├── COPYING ├── MANIFEST.in ├── Makefile ├── NEWS.txt ├── README.rst ├── README.txt ├── bin ├── pc_query ├── pycard-import └── pycardsyncer ├── doc ├── about.rst ├── faq.rst ├── installation.rst ├── license.rst ├── man │ ├── Makefile │ ├── pc_query.1 │ ├── pc_query.txt │ ├── pycard-import.1 │ ├── pycard-import.txt │ ├── pycardsyncer.1 │ └── pycardsyncer.txt └── usage.rst ├── pycard.conf.sample ├── pycarddav ├── __init__.py ├── backend.py ├── carddav.py ├── controllers │ ├── __init__.py │ ├── query.py │ └── sync.py ├── model.py └── ui.py ├── requirements.txt ├── setup.py └── tests ├── README.rst ├── assets ├── README.txt ├── configs │ └── base.conf ├── dexter.vcf ├── gödel.vcf ├── hanz.vcf └── lenna.vcf ├── local ├── output │ ├── serialize_to_vcf.out │ ├── vcard_insert1.out │ └── vcard_insert_with_status.out ├── pycard_test.py └── pycarddav_test.py └── vagrant ├── README.rst ├── Vagrantfile ├── Vagrantfile.pkg └── test_carddav.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .*.swp 3 | *.tgz 4 | pycarddav/version.py 5 | dist/ 6 | MANIFEST 7 | build/ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | python: 4 | - 2.6 5 | - 2.7 6 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 7 | install: 8 | - pip install -r requirements.txt --use-mirrors 9 | - pip install . 10 | - "if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install argparse; fi" 11 | # command to run tests, e.g. python setup.py test 12 | script: py.test tests/local/ 13 | branches: 14 | except: 15 | - webpage 16 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 0.7 2 | searching in the vcard chooser when merging addresses (press '/') 3 | nice speedup in pc_query, thanks to Leandro Lucarella 4 | better support for broken vcards 5 | auto creating missing db directories 6 | more bugfixes 7 | 8 | 0.6 9 | assorted bugfixes, see git log 10 | keyring support 11 | 12 | 0.5.1 13 | assorted bugfixes, see the git log 14 | 15 | 0.5 16 | multi account support 17 | support for more contenttypes/carddav servers 18 | all vcards should have a UID (required by RFC 6352 and enforced by Owncloud 5) 19 | 20 | 0.4.2 21 | searching for non ascii characters should work 22 | doing an OPTIONS request now to check for carddav capabilities 23 | this should speed up the syncing process and increase compatibility (SOGo) 24 | 25 | 0.4.1 26 | assorted bugfixes, see the git log 27 | 28 | 0.4.0 29 | experimental write support in the backend 30 | import & export vcards 31 | import directly from mutt 32 | speed increase in (initial) sync due to switching from pycurl to requests 33 | detects removed cards on server and deletes them locally 34 | can delete cards locally and on server 35 | can handle base64 encoded images/sounds etc. 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.txt: -------------------------------------------------------------------------------- 1 | Submitting a Bug 2 | ================ 3 | 4 | If you found a bug or any part of pycarddav isn't working as you 5 | expected, please check if that bug is either already reported at 6 | 7 | https://github.com/geier/pycarddav/issues?state=open 8 | 9 | or is already fixed on github. 10 | 11 | You can check it out and install via: 12 | 13 | git clone https://github.com/geier/pycarddav 14 | cd pycarddav 15 | python setup.py install 16 | 17 | If the bug persists, always run the command again with the --debug option 18 | and paste the output of that (of course you can edit out any private 19 | details like your username and resource). 20 | 21 | Also, it is often helpful if you include which OS you are on, which 22 | version of python and, in the case the problems occur during sync, which 23 | version of requests you are using. You can just run the file at 24 | https://gist.github.com/geier/5814123#file-debug_helper-py 25 | and paste the 26 | output. 27 | 28 | If the error occurs during sync, please also supply details on your 29 | CardDAV server (which server and version). 30 | 31 | 32 | Hacking 33 | ======= 34 | 35 | Before submitting your first patch, please add yourself to 36 | *CONTRIBUTORS.txt*. 37 | 38 | You can submit patches either via email (pycarddav at lostpackets dot de) 39 | or via github pull requests. 40 | -------------------------------------------------------------------------------- /CONTRIBUTORS.txt: -------------------------------------------------------------------------------- 1 | Christian Geier 2 | David Soulayrol - david.soulayrol [at] gmail [dot] com - http://david.soulayrol.name 3 | Aurélien Gâteau - http://agateau.com 4 | Hugo Osvaldo Barrera 5 | Ben Boeckel - mathstuf [at] gmail [dot] com 6 | Thomas Glanzmann - thomas@glanzmann.de - http://thomas.glanzmann.de 7 | Johannes Goetzfried - johannes@jgoetzfried.de - http://jgoetzfried.de 8 | Steven Allen - http://stebalien.com 9 | Jamie McClelland - jm@mayfirstorg - http://current.workingdirectory.net 10 | Leandro Lucarella - luca@llucax.com.ar - http://www.llucax.com.ar/ 11 | Tobias Röttger - toroettg@gmail.com - http://www.roettger-it.de/ 12 | Travis Parker - travis.parker@gmail.com 13 | Christian Burkert - post@cburkert.de 14 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2011-2012 Christian Geier, David Soulayrol 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include pycard.conf.sample 2 | include README.rst 3 | include CONTRIBUTING.txt 4 | include CONTRIBUTORS.txt 5 | include COPYING 6 | include NEWS.txt 7 | recursive-include doc/man *.1 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: README.rst 2 | 3 | README.rst: doc/about.rst doc/usage.rst doc/license.rst doc/installation.rst 4 | cat doc/about.rst doc/installation.rst doc/usage.rst doc/license.rst > README.rst 5 | -------------------------------------------------------------------------------- /NEWS.txt: -------------------------------------------------------------------------------- 1 | News 2 | ==== 3 | 05.02.2014: pyCardDAV v0.7.0 released 4 | 5 | Have a look at the Changelog. 6 | 7 | 26.11.2013: pyCardDAV v0.6.1 released 8 | 9 | The man pages should now be included in the release tarball. 10 | 11 | 25.11.2013: pyCardDAV v0.6 released 12 | 13 | This is mostly a bug fix release (thanks to Jamie McClelland for fixing 14 | two bugs), but also introduces keyring support (thanks to Steven Allen). 15 | Have a look at the README for further information on keyring support. 16 | 17 | 02.09.2013: pyCardDAV v.0.5.1 released 18 | 19 | pyCardDAV v0.5.1 is released. This is a bugfix release, if everything works 20 | fine for you, there is no need to upgrade. Database deletion should not be 21 | necessary. 22 | 23 | 15.06.2013: pyCardDAV v.0.5.0 released 24 | 25 | **New** This release brings support for multiple CardDAV accounts. See the 26 | usage instructions, the supplied example config and/or pc_query --help. Also 27 | support for more CardDAV servers is included. If you are upgrading, you need to 28 | delete the local database, otherwise pyCardDAV will refuse to work. 29 | 30 | **Attention** In accordance with RFC 6352 all VCards that are imported 31 | or changed by pyCadDAV will automatically get a random UID (if they 32 | haven't one already), as some CardDAV servers, e.g. Owncloud require 33 | these. 34 | 35 | 28.03.2013: pyCardDAV v0.4.1 released 36 | 37 | pyCardDAV v0.4.1 is released. This is a bugfix release, if everything works 38 | fine for you, there is no need to upgrade. Database deletion should not be 39 | necessary. 40 | 41 | 15.11.2012: pyCardDAV v0.4 released 42 | 43 | pyCardDAV v0.4 is released. This is a mayor rewrite (again), so some previously 44 | fixed bugs might be back in. If your upgrading, you should delete your database 45 | file first. 46 | 47 | On the plus side, there are some new features in pyCardDAV: 48 | 49 | experimental write support in the backend 50 | import & export vCards 51 | import addresses directly from mutt 52 | speed increase in (initial) sync due to switching from pycurl to requests 53 | detects removed cards on server and delete them locally 54 | can delete cards locally and then on server 55 | 56 | Also the license has changed to MIT/Expat, see the COPYING file for details 57 | (but a beer is still appreciated). 58 | 59 | PyCurl is not required anymore, pyCardDAV relies on requests now (which needs 60 | to be installed). 61 | 62 | Special thanks to David Soulayrol who made a lot of this happen. 63 | 64 | Attention: please make sure you have a backup when you enable write support, see 65 | Usage for more details. 66 | 67 | 27.01.2012: pyCardDAV v0.3.3_ released: 68 | **New** sabredav/owncloud support, thanks Davide Gerhard. 69 | 70 | Fixes a bug where properties with no type parameters were not printed. 71 | 72 | This release also fixes a small database bug. 73 | 74 | The config file has a new entry (*davserver*) which you can set to 75 | either davical or sabredav (depending on your CardDAV server). 76 | 77 | Future: 78 | The source code has been cleaned up quite a bit (nearly every line of code 79 | has been touched) and some features have been added. Write support is nearly 80 | finished in the backend (but will probably not be included in the next 81 | release yet), but the frontend is still really buggy and a pain to use. If 82 | you want to have a look, check the repository out at github and check the 83 | branch *write_support* (but it might me broken). 84 | 85 | 06.01.2012: pyCardDAV v0.3.2_ released: 86 | this is a minor bugfix update, db deleting should not be necessary. If 87 | everything is working fine at the moment, there is no need to upgrade. 88 | 89 | 06.01.2012: pyCardDAV v0.3.1_ released: 90 | this bugfix release fixes some bugs on Debian and a formatting bug (thanks to 91 | Antoine Sirinelli) and one more unicode bug (thanks to Thomas Klausner). Also, 92 | some more meaningful error messages were added. 93 | 94 | **Attention** if you are upgrading: 95 | you should delete the old database again and resync using pycardsyncer 96 | 97 | 08.12.2011: pyCardDAV v0.3_ released: 98 | this fixes an unicode bug and has a lot of internal changes 99 | 100 | **Attention** pc-query has been renamed to pc_query, 101 | make sure to delete the old database, also the config file 102 | format has somewhat changed 103 | 104 | 10.10.2011: pyCardDAV v0.2.1_ released 105 | this fixes a minor bug in the example config file 106 | 107 | 14.09.2011: pyCardDAV v0.2_ released 108 | **New** config files are now supported 109 | 110 | 13.09.2011: pyCardDAV moved to github_ 111 | feel free to fork etc. 112 | 113 | 12.08.2011: pyCardDAV v0.1_ released 114 | first public version 115 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | This project is dead 2 | ==================== 3 | 4 | pyCardDAV is deprecated. See `the relevant issue 5 | `_. A good alternative is `khard 6 | `_. 7 | 8 | I will still fix bugs against Davical for the foreseeable future, but don't 9 | expect anything else. 10 | 11 | About 12 | ===== 13 | *pyCardDAV* is a simple to use CardDAV_ CLI client. It has built in support for 14 | mutt's *query_command* but also works very well solo (and with other MUAs). 15 | 16 | *pyCardDAV* consists of *pycardsyncer*, a program for syncing your CardDAV 17 | resource into a local database and of *pc_query*, a program for querying the 18 | local database. *pyCardDAV* is some ugly python_ code (actually, it's not 19 | *that* bad anymore…) that holds together vobject_, lxml_, requests_ and 20 | pysqlite_. 21 | 22 | .. _CardDAV: http://en.wikipedia.org/wiki/CardDAV 23 | .. _python: http://python.org/ 24 | .. _vobject: http://vobject.skyhouseconsulting.com/ 25 | .. _lxml: http://lxml.de/ 26 | .. _pysqlite: http://code.google.com/p/pysqlite/ 27 | .. _requests: http://python-requests.org 28 | 29 | Features 30 | -------- 31 | (or rather: limitations) 32 | 33 | - *pyCardDAV* is only tested against davical, owncloud and sabredav 34 | - *pyCardDAV* can import the sender's address directly from mutt 35 | - *pyCardDAV* can backup and import to/from .vcf files 36 | - *pyCardDAV* can add email addresses directly from mutt 37 | - *pyCardDAV* only understands VCard 3.0 38 | - *pyCardDAV* is not python 3 compatible yet 39 | 40 | Feedback 41 | -------- 42 | Please do provide feedback if *pyCardDAV* works for you or even more importantly 43 | if it doesn't. You can reach me by email at pycarddav (at) lostpackets (dot) de , by 44 | jabber/XMPP at geier (at) jabber (dot) lostpackets (dot) de or via github_ 45 | 46 | .. _github: https://github.com/geier/pycarddav/ 47 | 48 | Installation 49 | ------------ 50 | You can download *pyCardDAV* either from the above download link or check it 51 | out from git (at github). Then install *pyCardDAV* by executing *python setup.py install*. 52 | If you feel more adventurous you can always the *develop* branch on github, which 53 | *should* always be in a usable state. pyCardDAV is also available on pypi_ and can 54 | be installed via pip install pycarddav or easy_install pycarddav. 55 | 56 | Copy and edit the supplied pycard.conf.sample file (default location is 57 | ~/.config/pycard/pycard.conf). If you don't want to store the password in 58 | clear text in the config file, *pyCardDAV* will ask for it while syncing. 59 | 60 | Make sure you have sqlite3 (normally available by default), vobject, lxml(>2), 61 | requests (>0.10), urwid (>0.9) pyxdg, installed. Users of python 2.6 will also 62 | need to install argparse. 63 | 64 | *pyCardDAV* has so far been successfully tested on recent versions of FreeBSD, 65 | NetBSD, Debian and Ubuntu with python 2.6 and 2.7 and against davical 0.9.9.4 - 66 | 1.0.1 (later versions should be ok, too, but 0.9.9.3 and earlier don't seem 67 | to work), owncloud and sabredav. 68 | 69 | .. _pypi: https://pypi.python.org/pypi/pyCardDAV/ 70 | .. _git: http://github.com/geier/pycarddav/ 71 | 72 | Usage 73 | ----- 74 | *pyCardDAV* consists of three scripts, *pycardsyncer* which is used to sync the 75 | local database with the server, *pc_query* to interact with the local database 76 | and *pycard-import* to import email addresses from mutt. 77 | 78 | Execute pycardsyncer to sync your addresses to the local database. You can test 79 | pc_query with:: 80 | 81 | % pc_query searchstring 82 | 83 | By default *pyCardDAV* only prints the names, email addresses and telephone 84 | numbers of contacts matching the search string, to see all vCard properties use 85 | the "-A" option. 86 | 87 | 88 | For usage with mutt etc., *pyCardDAV* can also print only email addresses in a 89 | mutt friendly format (with the "-m" option). Edit your mutt configuration so 90 | that query_command uses pc_query: 91 | 92 | Example from .muttrc:: 93 | 94 | set query_command="/home/username/bin/pc_query -m %s" 95 | 96 | The current version features experimental write support. If you want to 97 | test this, first make sure **you have a backup of your data** (but please do 98 | *NOT* rely on *pc_query --backup* for this just yet), then you can put the 99 | line:: 100 | 101 | write_support = YesPleaseIDoHaveABackupOfMyData 102 | 103 | in your config file (needs to be put into each *Account* section you want to 104 | enable write support for). 105 | 106 | You can also import, delete or backup single cards (backup also works for the 107 | whole collection, but please don't rely on it just yet). See *pc_query --help* 108 | for how to use these and for some more options. 109 | 110 | *pycarddav* can be configured to use different CardDAV accounts, see the example 111 | config for details. An account can be specified with *-a account_name* with all 112 | three utilies. If no account is chosen all searching and syncing actions will 113 | use all configured accounts, while on adding cards the first configured account 114 | will be used. 115 | 116 | Keyring support 117 | --------------- 118 | 119 | *pycarddav* supports keyring_, (version >=3.0). To use it, you need to add a 120 | password to the keyring via:: 121 | 122 | keyring set pycarddav:$account $username 123 | 124 | where $account is the name of an account as configured in your configuration 125 | file and $username is the corresponding username (and then have no password 126 | configured for that account). For more details on configuring keyring have a 127 | look at its documentation_. 128 | 129 | .. _keyring: https://pypi.python.org/pypi/keyring 130 | .. _documentation: https://pypi.python.org/pypi/keyring 131 | 132 | External Password Manager Support 133 | --------------------------------- 134 | Pycarddav can read passwords from the standard output of another programe (e.g. a password manager). 135 | Set `passwd_cmd` in the configuration file (and see the example there). 136 | 137 | Import Addresses from Mutt 138 | -------------------------- 139 | You can directly add sender addresses from mutt to *pyCardDAV*, either adding 140 | them to existing contacts or creating a new one. If write support is enabled, 141 | they will be uploaded on the server during the next sync. 142 | 143 | Example from .muttrc:: 144 | 145 | macro index,pager A "pycard-import" "add sender address to pycardsyncer" 146 | 147 | SSL 148 | --- 149 | If you use SSL to interact with your CardDAV Server (you probably should) and 150 | you don't have a certificate signed by a CA your OS Vendor trusts (like a 151 | self-signed certificate or one signed by CAcert) you can set *verify* to a path 152 | to the CA's root file (must be in pem format). If you don't want any certificate 153 | checking set *verify* to *false* to disable *any* ssl certificate checking (this 154 | is not recommended). 155 | 156 | Conflict Resolution 157 | ------------------- 158 | In case of conflicting edits (local VCard changed while remote VCard also 159 | changed), are "resolved" by pycarddav through overwriting the local VCard with 160 | the remote one (meaning local edits are lost in this case). Syncing more 161 | frequently can prevent this. 162 | 163 | Additional Information 164 | ---------------------- 165 | For now, VCard properties that have no value are not shown. 166 | 167 | Also, you should be able to use *pyCardDAV*'s CardDAV implementation for other 168 | projects. See the *CardDAV* class in *pycarddav/carddav.py*. 169 | 170 | In accordance with RFC 6352 all VCards that are imported or changed by pyCardDAV 171 | will automatically get a random UID (if they haven't one already), as some 172 | CardDAV servers, e.g. Owncloud require these. 173 | 174 | Debian Wheezy Quickstart 175 | ------------------------ 176 | 177 | On Debian based Linuxes this will set you up:: 178 | 179 | apt-get install python-requests python-vobject python-pytest python-urwid python-lxml python-pyxdg 180 | sudo python setup.py install 181 | mkdir -p ~/.config/pycard 182 | chmod 700 ~/.config/pycard 183 | cp pycard.conf.sample ~/.config/pycard/pycard.conf 184 | 185 | License 186 | ------- 187 | *pyCardDAV* is released under the Expat/MIT License: 188 | 189 | Copyright (c) 2011-2014 Christian Geier and contributors 190 | 191 | Permission is hereby granted, free of charge, to any person obtaining 192 | a copy of this software and associated documentation files (the 193 | "Software"), to deal in the Software without restriction, including 194 | without limitation the rights to use, copy, modify, merge, publish, 195 | distribute, sublicense, and/or sell copies of the Software, and to 196 | permit persons to whom the Software is furnished to do so, subject to 197 | the following conditions: 198 | 199 | The above copyright notice and this permission notice shall be 200 | included in all copies or substantial portions of the Software. 201 | 202 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 203 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 204 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 205 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 206 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 207 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 208 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 209 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | README.rst -------------------------------------------------------------------------------- /bin/pc_query: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # vim: set ts=4 sw=4 expandtab sts=4: 3 | # Copyright (c) 2011-2014 Christian Geier & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | try: 25 | import argparse 26 | import logging 27 | import sys 28 | 29 | from ConfigParser import NoSectionError 30 | 31 | from pycarddav import ConfigurationParser 32 | from pycarddav import capture_user_interruption 33 | from pycarddav.controllers.query import query 34 | 35 | except ImportError as error: 36 | sys.stderr.write(str(error)) 37 | sys.exit(1) 38 | 39 | 40 | class QueryConfigurationParser(ConfigurationParser): 41 | """A specialized setup tool for cards query.""" 42 | where_options = ('vcard', 'name', 'fname', 'allnames') 43 | def __init__(self): 44 | ConfigurationParser.__init__(self, "prints contacts cards matching a search string", 45 | check_accounts=False) 46 | 47 | self._arg_parser.add_argument( 48 | "-A", action="store_true", dest="query__display_all", default=False, 49 | help="prints the whole card, not only name, " 50 | "telephone numbers and email addresses") 51 | self._arg_parser.add_argument( 52 | "-m", action="store_true", dest="query__mutt_format", default=False, 53 | help="only prints email addresses, in a mutt friendly format") 54 | self._arg_parser.add_argument( 55 | "-t", action="store_true", dest="query__tel", default=False, 56 | help="only prints telephone number, analogue to -m " 57 | "(but in different sequence)") 58 | # self._arg_parser.add_argument( 59 | # "-e", action="store_true", dest="query__edit", default=False, 60 | # help="edit the contact file.\n" 61 | # "NOTE: this feature is experimental and will probably corrupt " 62 | # "your *local* database. Your remote CardDAV resource will stay " 63 | # "untouched, as long as You don't enable write support for the " 64 | # "syncer.") 65 | self._arg_parser.add_argument( 66 | "query__search_string", metavar="SEARCHSTRING", default="", 67 | help="the string to search for", nargs="?") 68 | self._arg_parser.add_argument( 69 | "-b", "--backup", action="store", dest="query__backup", 70 | metavar="BACKUP", help="backup the local db to BACKUP, " 71 | "if a SEARCHSTRING is present, only backup cards matching it.") 72 | self._arg_parser.add_argument( 73 | "-i", "--import", metavar="FILE", 74 | type=argparse.FileType("r"), dest="query__importing", 75 | help="import vcard from FILE or STDIN into the first specified account") 76 | self._arg_parser.add_argument( 77 | "--delete", dest="query__delete", action="store_true", 78 | help="delete card matching SEARCHSTRING") 79 | self._arg_parser.add_argument( 80 | "-w", "--where", dest="query__where", metavar="WHERE", 81 | choices=self.where_options, 82 | help="Query for matches in the WHERE field(s)") 83 | self._arg_parser.add_argument( 84 | "-a", "--account", action="append", dest="sync__accounts", 85 | metavar="NAME", help="use only the NAME account (can be used more than once)") 86 | 87 | def check(self, ns): 88 | result = ConfigurationParser.check(self, ns) 89 | 90 | if ns.query.where not in self.where_options: 91 | logging.error("Invalid 'where' option '%s', should be one of %s", 92 | ns.query.where, 93 | ', '.join(repr(o) for o in self.where_options)) 94 | result = False 95 | 96 | accounts = [account.name for account in ns.accounts] 97 | if ns.sync.accounts: 98 | for name in set(ns.sync.accounts): 99 | if not name in [a.name for a in ns.accounts]: 100 | logging.warn('Uknown account %s', name) 101 | ns.sync.accounts.remove(name) 102 | if len(ns.sync.accounts) == 0: 103 | logging.error('No valid account selected') 104 | result = False 105 | else: 106 | ns.sync.accounts = accounts 107 | 108 | ns.sync.accounts = list(set(ns.sync.accounts)) 109 | 110 | return result 111 | 112 | def _read_configuration(self, overrides): 113 | """Build the configuration holder.""" 114 | ns = ConfigurationParser._read_configuration(self, overrides) 115 | 116 | if ns.query.where is None: 117 | ns.query.where = 'vcard' 118 | try: 119 | where = self._conf_parser.get('query', 'where') 120 | ns.query.where = where 121 | except (ValueError, NoSectionError): 122 | pass 123 | 124 | return ns 125 | 126 | 127 | capture_user_interruption() 128 | 129 | # Read configuration. 130 | conf_parser = QueryConfigurationParser() 131 | conf = conf_parser.parse() 132 | if conf is None: 133 | sys.exit(1) 134 | 135 | if conf.debug: 136 | conf_parser.dump(conf) 137 | 138 | query(conf) 139 | -------------------------------------------------------------------------------- /bin/pycard-import: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*- coding: utf-8 -*- 3 | # vim: set ts=4 sw=4 expandtab sts=4: 4 | # Copyright (c) 2011-2014 Christian Geier & contributors 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files (the 8 | # "Software"), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so, subject to 12 | # the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be 15 | # included in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | """A pyCardDAV tool to create VCards from one email. 26 | """ 27 | 28 | 29 | import email 30 | import email.header 31 | import os 32 | import sys 33 | import traceback 34 | import logging 35 | 36 | import pycarddav 37 | import pycarddav.backend 38 | import pycarddav.model 39 | import pycarddav.ui 40 | 41 | 42 | class ImportConfigurationParser(pycarddav.ConfigurationParser): 43 | """A specialized setup tool for importing a contact.""" 44 | def __init__(self): 45 | pycarddav.ConfigurationParser.__init__(self, 'Import contacts from a mail on input.', 46 | check_accounts=False) 47 | 48 | self._arg_parser.epilog = 'Only the From header is parsed if no header is specified.' 49 | self._arg_parser.add_argument( 50 | '--batch', action='store_true', dest='batch', default=False, 51 | help='do not open the editor') 52 | self._arg_parser.add_argument( 53 | '-n', '--dry-run', action='store_true', dest='dry_run', default=False, 54 | help='do not actually update the database (implies --batch)') 55 | self._arg_parser.add_argument( 56 | "-a", "--account", action="store", dest="sync__account", 57 | metavar="NAME", help="add to NAME account (can be used only once), if no valid account name is given, the first one from the config will be used") 58 | # Headers selection. To: is the default. 59 | self._arg_parser.add_argument( 60 | '-f', '--from', action='append_const', dest='headers', const='From', 61 | help='import the content of the From header') 62 | self._arg_parser.add_argument( 63 | '-t', '--to', action='append_const', dest='headers', const='To', 64 | help='import the content of the To header') 65 | self._arg_parser.add_argument( 66 | '--cc', action='append_const', dest='headers', const='Cc', 67 | help='import the content of the Cc header') 68 | self._arg_parser.add_argument( 69 | '--bcc', action='append_const', dest='headers', const='Bcc', 70 | help='import the content of the Bcc header') 71 | 72 | def check(self, conf): 73 | accounts = [account.name for account in conf.accounts] 74 | if conf.sync.account: 75 | if conf.sync.account not in accounts: 76 | logging.critical(conf.sync.account + ' is not a valid account') 77 | sys.exit(1) 78 | else: 79 | conf.sync.account = accounts[0] 80 | 81 | if conf.dry_run: 82 | conf.batch = True 83 | if not conf.headers: 84 | conf.headers = ['From'] 85 | 86 | return pycarddav.ConfigurationParser.check(self, conf) 87 | 88 | 89 | def parse_address(header): 90 | """split a header line (To, From, CC) into email address and display name""" 91 | if header is None: 92 | return None, '' 93 | 94 | address_string = [] 95 | addresses = email.header.decode_header(header) 96 | for string, enc in addresses: 97 | try: 98 | string = string.decode(enc) 99 | except TypeError: 100 | try: 101 | string = unicode(string) 102 | except UnicodeDecodeError: 103 | string = string.decode('ascii', 'replace') 104 | address_string.append(string) 105 | 106 | address_string = ' '.join(address_string) 107 | display_name, address = email.utils.parseaddr(address_string) 108 | 109 | return address, display_name 110 | 111 | 112 | def capture_tty(): 113 | """Walk the parent processes until a TTY is found. 114 | """ 115 | sys.stdin = open('/dev/tty') 116 | sys.stdout = open('/dev/tty', 'wb') 117 | sys.stderr = open('/dev/tty', 'wb') 118 | 119 | os.dup2(sys.stdin.fileno(), 0) 120 | os.dup2(sys.stdout.fileno(), 1) 121 | os.dup2(sys.stderr.fileno(), 2) 122 | 123 | 124 | def release_tty(): 125 | """closing the files""" 126 | sys.stdin.close() 127 | sys.stdout.close() 128 | sys.stderr.close() 129 | 130 | 131 | def do_import(): 132 | """does the real work""" 133 | conf = ImportConfigurationParser().parse() 134 | if conf is None: 135 | sys.exit(1) 136 | db = pycarddav.backend.SQLiteDb(conf.sqlite.path, "utf-8", "stricts", False) 137 | 138 | msg = email.message_from_string(sys.stdin.read()) 139 | 140 | if not conf.batch: 141 | capture_tty() 142 | try: 143 | for header in conf.headers: 144 | address, display_name = parse_address(msg[header]) 145 | if address is None: 146 | continue 147 | vcard = pycarddav.model.vcard_from_email(display_name, address) 148 | if conf.batch: 149 | if not conf.dry_run: 150 | db.update(vcard, conf.sync.account, vcard.href, status=pycarddav.backend.NEW) 151 | else: 152 | pycarddav.ui.start_pane(pycarddav.ui.EditorPane(db, conf.sync.account, vcard)) 153 | 154 | except Exception: 155 | exc_type, exc_value, exc_tb = sys.exc_info() 156 | traceback.print_exception(exc_type, exc_value, exc_tb, file=sys.stdout) 157 | finally: 158 | if not conf.batch: 159 | release_tty() 160 | 161 | if __name__ == '__main__': 162 | do_import() 163 | -------------------------------------------------------------------------------- /bin/pycardsyncer: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # vim: set ts=4 sw=4 expandtab sts=4: 3 | # Copyright (c) 2011-2014 Christian Geier & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | try: 25 | import logging 26 | import sys 27 | 28 | from pycarddav import SyncConfigurationParser 29 | from pycarddav import capture_user_interruption 30 | from pycarddav.controllers.sync import sync 31 | 32 | except ImportError as error: 33 | sys.stderr.write(str(error)) 34 | sys.exit(1) 35 | 36 | 37 | 38 | capture_user_interruption() 39 | 40 | # Read configuration. 41 | conf_parser = SyncConfigurationParser() 42 | conf = conf_parser.parse() 43 | if conf is None: 44 | sys.exit(1) 45 | 46 | if conf.debug: 47 | conf_parser.dump(conf) 48 | 49 | rvalue = 0 50 | for one in conf.accounts: 51 | if one.name in conf.sync.accounts: 52 | logging.debug('start syncing account {0}'.format(one.name)) 53 | conf.account = one 54 | try: 55 | sync(conf) 56 | except Exception as error: 57 | if conf.debug: 58 | raise 59 | logging.critical('While syncing account "{0}" an error occured:\n '.format(one.name) + str(error)) 60 | rvalue = 1 61 | 62 | sys.exit(rvalue) 63 | -------------------------------------------------------------------------------- /doc/about.rst: -------------------------------------------------------------------------------- 1 | About 2 | ===== 3 | *pyCardDAV* is a simple to use CardDAV_ CLI client. It has built in support for 4 | mutt's *query_command* but also works very well solo. 5 | 6 | *pyCardDAV* consists of *pycardsyncer*, a program for syncing your CardDAV 7 | resource into a local database and of *pc_query*, a program for querying the 8 | local database. *pyCardDAV* is some ugly python_ code (actually, it's not 9 | *that* bad anymore…) that holds together vobject_, lxml_, requests_ and 10 | pysqlite_. 11 | 12 | .. _CardDAV: http://en.wikipedia.org/wiki/CardDAV 13 | .. _python: http://python.org/ 14 | .. _vobject: http://vobject.skyhouseconsulting.com/ 15 | .. _lxml: http://lxml.de/ 16 | .. _pysqlite: http://code.google.com/p/pysqlite/ 17 | .. _requests: http://python-requests.org 18 | 19 | Features 20 | -------- 21 | (or rather: limitations) 22 | 23 | - *pyCardDAV* can only use one address book resource at the moment 24 | - *pyCardDAV* is only tested against davical, owncloud and sabredav 25 | - *pyCardDAV* can import the sender's address directly from mutt 26 | - *pyCardDAV* can backup and import to/from .vcf files 27 | - *pyCardDAV* can add email addresses directly from mutt 28 | - *pyCardDAV* only understands VCard 3.0 29 | - *pyCardDAV* is not python 3 compatible yet 30 | 31 | Feedback 32 | -------- 33 | Please do provide feedback if *pyCardDAV* works for you or even more importantly 34 | if it doesn't. You can reach me by email at pycarddav (at) lostpackets (dot) de , by 35 | jabber/XMPP at geier (at) jabber (dot) ccc (dot) de or via github_ 36 | 37 | .. _github: https://github.com/geier/pycarddav/ 38 | 39 | -------------------------------------------------------------------------------- /doc/faq.rst: -------------------------------------------------------------------------------- 1 | Frequently Asked Questions 2 | ========================== 3 | 4 | 5 | Which CardDAV Servers are supported by pycarddav? 6 | ------------------------------------------------- 7 | 8 | * Baïkal 9 | * (Apple's) CalendarServer 10 | * DAViCal 11 | * ownCloud 12 | * Radicale 13 | 14 | were working at least at one time in the past and still hopefully do. 15 | 16 | pyCardDAV exits with *KeyError: 'nonce'*, what do I do now? 17 | ----------------------------------------------------------- 18 | 19 | You need to upgrade your requests installation to at least requests 1.2.1 20 | -------------------------------------------------------------------------------- /doc/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ------------ 3 | You can download *pyCardDAV* either from the above download link or check it 4 | out from git (at github). Then install *pyCardDAV* by executing *python setup.py install*. 5 | If you feel more adventurous you can always the *develop* branch on github, which 6 | *should* always be in a usable state. pyCardDAV is also available on pypi_ and can 7 | be installed via pip install pycarddav or easy_install pycarddav. 8 | 9 | Copy and edit the supplied pycard.conf.sample file (default location is 10 | ~/.config/pycard/pycard.conf). If you don't want to store the password in 11 | clear text in the config file, *pyCardDAV* will ask for it while syncing. 12 | 13 | Make sure you have sqlite3 (normally available by default), vobject, lxml(>2), 14 | requests (>0.10), urwid (>0.9) pyxdg, installed. Users of python 2.6 will also 15 | need to install argparse. 16 | 17 | *pyCardDAV* has so far been successfully tested on recent versions of FreeBSD, 18 | NetBSD, Debian and Ubuntu with python 2.6 and 2.7 and against davical 0.9.9.4 - 19 | 1.0.1 (later versions should be ok, too, but 0.9.9.3 and earlier don't seem 20 | to work), owncloud and sabredav. 21 | 22 | .. _pypi: https://pypi.python.org/pypi/pyCardDAV/ 23 | .. _git: http://github.com/geier/pycarddav/ 24 | 25 | -------------------------------------------------------------------------------- /doc/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ------- 3 | *pyCardDAV* is released under the Expat/MIT License: 4 | 5 | Copyright (c) 2011-2013 Christian Geier and contributors 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining 8 | a copy of this software and associated documentation files (the 9 | "Software"), to deal in the Software without restriction, including 10 | without limitation the rights to use, copy, modify, merge, publish, 11 | distribute, sublicense, and/or sell copies of the Software, and to 12 | permit persons to whom the Software is furnished to do so, subject to 13 | the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be 16 | included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /doc/man/Makefile: -------------------------------------------------------------------------------- 1 | all: pycardsyncer.1 pc_query.1 pycard-import.1 2 | 3 | # view with 4 | # nroff -e -mandoc pycardsyncer.1 | less 5 | pycardsyncer.1: pycardsyncer.txt 6 | a2x -f manpage pycardsyncer.txt 7 | 8 | pc_query.1: pc_query.txt 9 | a2x -f manpage pc_query.txt 10 | 11 | pycard-import.1: pycard-import.txt 12 | a2x -f manpage pycard-import.txt 13 | -------------------------------------------------------------------------------- /doc/man/pc_query.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Title: pc_query 3 | .\" Author: [see the "AUTHOR" section] 4 | .\" Generator: DocBook XSL Stylesheets v1.76.1 5 | .\" Date: 01/31/2014 6 | .\" Manual: \ \& 7 | .\" Source: \ \& 8 | .\" Language: English 9 | .\" 10 | .TH "PC_QUERY" "1" "01/31/2014" "\ \&" "\ \&" 11 | .\" ----------------------------------------------------------------- 12 | .\" * Define some portability stuff 13 | .\" ----------------------------------------------------------------- 14 | .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | .\" http://bugs.debian.org/507673 16 | .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html 17 | .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | .ie \n(.g .ds Aq \(aq 19 | .el .ds Aq ' 20 | .\" ----------------------------------------------------------------- 21 | .\" * set default formatting 22 | .\" ----------------------------------------------------------------- 23 | .\" disable hyphenation 24 | .nh 25 | .\" disable justification (adjust text to left margin only) 26 | .ad l 27 | .\" ----------------------------------------------------------------- 28 | .\" * MAIN CONTENT STARTS HERE * 29 | .\" ----------------------------------------------------------------- 30 | .SH "NAME" 31 | pc_query \- query the local address book 32 | .SH "SYNOPSIS" 33 | .sp 34 | \fBpc_query\fR [\fIOPTIONS\fR] [SEARCHSTRING] 35 | .SH "DESCRIPTION" 36 | .sp 37 | 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\&. 38 | .SH "OPTIONS" 39 | .SS "BASIC STARTUP OPTIONS" 40 | .PP 41 | \fB\-a, \-\-account\fR=\fINAME\fR 42 | .RS 4 43 | Only use the account/resource 44 | \fINAME\fR 45 | (this option can be used more than once)\&. 46 | .RE 47 | .PP 48 | \fB\-c, \-\-config\fR=\fICONFIG\fR 49 | .RS 4 50 | Use the configuration file 51 | \fICONFIG\fR, otherwise pc_query(1) will look in 52 | \fI$HOME/\&.pycard/\fR 53 | and 54 | \fI$HOME/\&.config/pycard/\fR 55 | for files named 56 | \fIpycard\&.conf\fR\&. 57 | .RE 58 | .PP 59 | \fB\-\-debug\fR 60 | .RS 4 61 | This option enables debugging output\&. 62 | .RE 63 | .PP 64 | \fB\-h, \-\-help\fR 65 | .RS 4 66 | Print a small help text and exit\&. 67 | .RE 68 | .PP 69 | \fB\-v, \-\-version\fR 70 | .RS 4 71 | Print pc_query\(cqs version number and exit\&. 72 | .RE 73 | .SS "PRINTING OPTIONS" 74 | .sp 75 | 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\&. 76 | .PP 77 | \fB\-A\fR 78 | .RS 4 79 | Prints the whole card\&. 80 | .RE 81 | .PP 82 | \fB\-m\fR 83 | .RS 4 84 | Only prints names and email addresses, in a mutt friendly format (one line per email address)\&. 85 | .RE 86 | .PP 87 | \fB\-t\fR 88 | .RS 4 89 | Only prints names and telephone numbers, analogue to \-m (but in different sequence) 90 | .RE 91 | .SS "FILE INPUT AND OUTPUT OPTIONS" 92 | .PP 93 | \fB\-b, \-\-backup\fR\fI=\*(AqFILE\fR 94 | .RS 4 95 | Backup the local db to BACKUP, if a SEARCHSTRING is present, only backup cards matching it\&. 96 | .RE 97 | .PP 98 | \fB\-\-delete\fR 99 | .RS 4 100 | Delete card matching 101 | \fISEARCHSTRING\fR, if more than one matches, the user has to choose one card in an interactive user interface\&. 102 | .RE 103 | .PP 104 | \fB\-i, \-\-import\fR=\fIFILE\fR 105 | .RS 4 106 | Import vCard from FILE or 107 | \fISTDIN\fR 108 | into the first specified (or default) account 109 | .RE 110 | .SS "OTHER OPTIONS" 111 | .PP 112 | \fB\-\-where\fR=\fIMODE\fR 113 | .RS 4 114 | Decides which part of the contact cards are matched against the searchterm\&. 115 | \fIMODE\fR 116 | is one of 117 | \fIvcard\fR, 118 | \fIname\fR 119 | (the structured name, as used in the vcard specification), 120 | \fIfname\fR 121 | (the formated name, meaning as is printed by pc_query) and 122 | \fIallnames\fR 123 | (\fIallnames\fR 124 | means search 125 | \fIfname\fR 126 | as well as 127 | \fIname\fR)\&. While only search through the names might not find what you are looking for, it will considerably speed up your query\&. 128 | .RE 129 | .SH "AUTHOR" 130 | .sp 131 | pc_query was mostly written by Christian Geier, with a lot of help by others, see \fICONTRIBUTORS\&.txt\fR in the pycarddav distribution\&. 132 | .SH "RESOURCES" 133 | .sp 134 | 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/\&. 135 | .SH "SEE ALSO" 136 | .sp 137 | pycardsyncer(1), pycard\-import(1) 138 | .SH "COPYING" 139 | .sp 140 | 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\&. 141 | -------------------------------------------------------------------------------- /doc/man/pc_query.txt: -------------------------------------------------------------------------------- 1 | PC_QUERY(1) 2 | =========== 3 | :doctype: manpage 4 | 5 | NAME 6 | ---- 7 | pc_query - query the local address book 8 | 9 | 10 | SYNOPSIS 11 | -------- 12 | *pc_query* ['OPTIONS'] [SEARCHSTRING] 13 | 14 | 15 | DESCRIPTION 16 | ----------- 17 | pc_query(1) prints contacts from a local address book, synchronized with 18 | remote CardDAV resources with pycardsyncer(1). Contact information is 19 | kept in the 'vCard' format, one set of information, corresponding to a 20 | business card, is therefore often referred to as a 'card'. pc_query(1) 21 | can also import and export vcards from/to the database. pc_query(1) is 22 | part of the 'pycarddav' package. 23 | 24 | 25 | OPTIONS 26 | ------- 27 | 28 | BASIC STARTUP OPTIONS 29 | ~~~~~~~~~~~~~~~~~~~~~ 30 | *-a, --account*='NAME':: 31 | Only use the account/resource 'NAME' (this option can be used more than 32 | once). 33 | *-c, --config*='CONFIG':: 34 | Use the configuration file 'CONFIG', otherwise pc_query(1) will look in 35 | '$HOME/.pycard/' and '$HOME/.config/pycard/' for files named 'pycard.conf'. 36 | *--debug*:: 37 | This option enables debugging output. 38 | *-h, --help*:: 39 | Print a small help text and exit. 40 | *-v, --version*:: 41 | Print pc_query's version number and exit. 42 | 43 | 44 | PRINTING OPTIONS 45 | ~~~~~~~~~~~~~~~~ 46 | These options can only be used with 'SEARCHSTRING' and determine the 47 | output format of pc_query(1). Without any options pc_query(1) will print 48 | the name, telephone numbers and email addresses of all matching contact 49 | cards. 50 | 51 | *-A*:: 52 | Prints the whole card. 53 | *-m*:: 54 | Only prints names and email addresses, in a mutt friendly format 55 | (one line per email address). 56 | *-t*:: 57 | Only prints names and telephone numbers, analogue to -m (but in different 58 | sequence) 59 | 60 | 61 | FILE INPUT AND OUTPUT OPTIONS 62 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 63 | 64 | *-b, --backup*'='FILE':: 65 | Backup the local db to BACKUP, if a SEARCHSTRING is present, only 66 | backup cards matching it. 67 | *--delete*:: 68 | Delete card matching 'SEARCHSTRING', if more than one matches, the 69 | user has to choose one card in an interactive user interface. 70 | *-i, --import*='FILE':: 71 | Import vCard from FILE or 'STDIN' into the first specified (or 72 | default) account 73 | 74 | OTHER OPTIONS 75 | ~~~~~~~~~~~~~ 76 | 77 | *--where*='MODE':: 78 | Decides which part of the contact cards are matched against the 79 | searchterm. 'MODE' is one of 'vcard', 'name' (the structured name, 80 | as used in the vcard specification), 'fname' (the formated name, meaning 81 | as is printed by pc_query) and 'allnames' ('allnames' means search 82 | 'fname' as well as 'name'). While only search through the names might 83 | not find what you are looking for, it will considerably speed up your 84 | query. 85 | 86 | AUTHOR 87 | ------ 88 | pc_query was mostly written by Christian Geier, with a lot of help by 89 | others, see 'CONTRIBUTORS.txt' in the pycarddav distribution. 90 | 91 | 92 | RESOURCES 93 | --------- 94 | Main web site: 95 | Please report bugs via the contact information at the above web site or 96 | via github: . 97 | 98 | 99 | SEE ALSO 100 | -------- 101 | pycardsyncer(1), pycard-import(1) 102 | 103 | 104 | COPYING 105 | ------- 106 | Copyright \(C) 2011-2014 Christian Geier and Contributors 107 | pc_query and pycarddav are released under the terms of the Expat/MIT 108 | license, see the 'COPYING' file distributed with pycarddav. 109 | -------------------------------------------------------------------------------- /doc/man/pycard-import.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Title: pycard-import 3 | .\" Author: [see the "AUTHOR" section] 4 | .\" Generator: DocBook XSL Stylesheets v1.76.1 5 | .\" Date: 11/25/2013 6 | .\" Manual: \ \& 7 | .\" Source: \ \& 8 | .\" Language: English 9 | .\" 10 | .TH "PYCARD\-IMPORT" "1" "11/25/2013" "\ \&" "\ \&" 11 | .\" ----------------------------------------------------------------- 12 | .\" * Define some portability stuff 13 | .\" ----------------------------------------------------------------- 14 | .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | .\" http://bugs.debian.org/507673 16 | .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html 17 | .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | .ie \n(.g .ds Aq \(aq 19 | .el .ds Aq ' 20 | .\" ----------------------------------------------------------------- 21 | .\" * set default formatting 22 | .\" ----------------------------------------------------------------- 23 | .\" disable hyphenation 24 | .nh 25 | .\" disable justification (adjust text to left margin only) 26 | .ad l 27 | .\" ----------------------------------------------------------------- 28 | .\" * MAIN CONTENT STARTS HERE * 29 | .\" ----------------------------------------------------------------- 30 | .SH "NAME" 31 | pycard-import \- import email addresses from mails from stdin 32 | .SH "SYNOPSIS" 33 | .sp 34 | \fBpycard\-import\fR [\fIOPTIONS\fR] 35 | .SH "DESCRIPTION" 36 | .sp 37 | 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\&. 38 | .SH "OPTIONS" 39 | .SS "BASIC STARTUP OPTIONS" 40 | .PP 41 | \fB\-a, \-\-account\fR=\fINAME\fR 42 | .RS 4 43 | Only use the account/resource 44 | \fINAME\fR 45 | (this option can be used more than once)\&. 46 | .RE 47 | .PP 48 | \fB\-c, \-\-config\fR=\fICONFIG\fR 49 | .RS 4 50 | Use the configuration file 51 | \fICONFIG\fR, otherwise pycard\-import(1) will look in 52 | \fI$HOME/\&.pycard/\fR 53 | and 54 | \fI$HOME/\&.config/pycard/\fR 55 | for files named 56 | \fIpycard\&.conf\fR\&. 57 | .RE 58 | .PP 59 | \fB\-\-debug\fR 60 | .RS 4 61 | This option enables debugging output\&. 62 | .RE 63 | .PP 64 | \fB\-h, \-\-help\fR 65 | .RS 4 66 | Print a small help text and exit\&. 67 | .RE 68 | .PP 69 | \fB\-v, \-\-version\fR 70 | .RS 4 71 | Print pycard\-import\(cqs version number and exit\&. 72 | .RE 73 | .SS "IMPORTING OPTIONS" 74 | .PP 75 | \fB\-\-batch\fR 76 | .RS 4 77 | Do not open the editor\&. 78 | .RE 79 | .PP 80 | \fB\-\-bcc\fR 81 | .RS 4 82 | Import the content of the Bcc header\&. 83 | .RE 84 | .PP 85 | \fB\-\-cc\fR 86 | .RS 4 87 | Import the content of the Cc header\&. 88 | .RE 89 | .PP 90 | \fB\-f, \-\-from\fR 91 | .RS 4 92 | Import the content of the From header\&. 93 | .RE 94 | .PP 95 | \fB\-n, \-\-dry\-run\fR 96 | .RS 4 97 | Do not actually update the database (implies \-\-batch)\&. 98 | .RE 99 | .PP 100 | \fB\-t, \-\-to\fR 101 | .RS 4 102 | Import the content of the To header\&. 103 | .RE 104 | .SH "AUTHOR" 105 | .sp 106 | pycard\-import was mostly written by Christian Geier, with a lot of help by others, see \fICONTRIBUTORS\&.txt\fR in the pycarddav distribution\&. 107 | .SH "RESOURCES" 108 | .sp 109 | 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/\&. 110 | .SH "SEE ALSO" 111 | .sp 112 | pycardsyncer(1), pc_query(1) 113 | .SH "COPYING" 114 | .sp 115 | 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\&. 116 | -------------------------------------------------------------------------------- /doc/man/pycard-import.txt: -------------------------------------------------------------------------------- 1 | PYCARD-IMPORT(1) 2 | ================ 3 | :doctype: manpage 4 | 5 | NAME 6 | ---- 7 | pycard-import - import email addresses from mails from stdin 8 | 9 | 10 | SYNOPSIS 11 | -------- 12 | *pycard-import* ['OPTIONS'] 13 | 14 | 15 | DESCRIPTION 16 | ----------- 17 | pycard-import(1) imports email addresses from mails piped into its stdin 18 | and saves those addresses to the database used by pycardsyncer(1) and 19 | pc_query(1). During non-batch import a choice is given to merge the 20 | email address with a contact or to create a new one. pycard-import(1) is 21 | part of the 'pycarddav' package. 22 | 23 | 24 | OPTIONS 25 | ------- 26 | 27 | BASIC STARTUP OPTIONS 28 | ~~~~~~~~~~~~~~~~~~~~~ 29 | *-a, --account*='NAME':: 30 | Only use the account/resource 'NAME' (this option can be used more than 31 | once). 32 | *-c, --config*='CONFIG':: 33 | Use the configuration file 'CONFIG', otherwise pycard-import(1) will look in 34 | '$HOME/.pycard/' and '$HOME/.config/pycard/' for files named 'pycard.conf'. 35 | *--debug*:: 36 | This option enables debugging output. 37 | *-h, --help*:: 38 | Print a small help text and exit. 39 | *-v, --version*:: 40 | Print pycard-import's version number and exit. 41 | 42 | 43 | IMPORTING OPTIONS 44 | ~~~~~~~~~~~~~~~~ 45 | *--batch*:: 46 | Do not open the editor. 47 | *--bcc*:: 48 | Import the content of the Bcc header. 49 | *--cc*:: 50 | Import the content of the Cc header. 51 | *-f, --from*:: 52 | Import the content of the From header. 53 | *-n, --dry-run*:: 54 | Do not actually update the database (implies --batch). 55 | *-t, --to*:: 56 | Import the content of the To header. 57 | 58 | 59 | AUTHOR 60 | ------ 61 | pycard-import was mostly written by Christian Geier, with a lot of help by 62 | others, see 'CONTRIBUTORS.txt' in the pycarddav distribution. 63 | 64 | 65 | RESOURCES 66 | --------- 67 | Main web site: 68 | Please report bugs via the contact information at the above web site or 69 | via github: . 70 | 71 | 72 | SEE ALSO 73 | -------- 74 | pycardsyncer(1), pc_query(1) 75 | 76 | 77 | COPYING 78 | ------- 79 | Copyright \(C) 2011-2014 Christian Geier and Contributors. 80 | pycard-import and pycarddav are released under the terms of the 81 | Expat/MIT license, see the 'COPYING' file distributed with pycarddav. 82 | -------------------------------------------------------------------------------- /doc/man/pycardsyncer.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Title: pycardsyncer 3 | .\" Author: [see the "AUTHOR" section] 4 | .\" Generator: DocBook XSL Stylesheets v1.76.1 5 | .\" Date: 11/25/2013 6 | .\" Manual: \ \& 7 | .\" Source: \ \& 8 | .\" Language: English 9 | .\" 10 | .TH "PYCARDSYNCER" "1" "11/25/2013" "\ \&" "\ \&" 11 | .\" ----------------------------------------------------------------- 12 | .\" * Define some portability stuff 13 | .\" ----------------------------------------------------------------- 14 | .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | .\" http://bugs.debian.org/507673 16 | .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html 17 | .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | .ie \n(.g .ds Aq \(aq 19 | .el .ds Aq ' 20 | .\" ----------------------------------------------------------------- 21 | .\" * set default formatting 22 | .\" ----------------------------------------------------------------- 23 | .\" disable hyphenation 24 | .nh 25 | .\" disable justification (adjust text to left margin only) 26 | .ad l 27 | .\" ----------------------------------------------------------------- 28 | .\" * MAIN CONTENT STARTS HERE * 29 | .\" ----------------------------------------------------------------- 30 | .SH "NAME" 31 | pycardsyncer \- synchronizes CardDAV resources to local db 32 | .SH "SYNOPSIS" 33 | .sp 34 | \fBpycardsyncer\fR [\fIOPTIONS\fR] 35 | .SH "DESCRIPTION" 36 | .sp 37 | pycardsyncer(1) synchronizes remote CardDAV resources to a local database that can then be queried and modified with pc_query(1)\&. 38 | .SH "OPTIONS" 39 | .PP 40 | \fB\-a, \-\-account\fR=\fINAME\fR 41 | .RS 4 42 | Sync only the account/resource 43 | \fINAME\fR 44 | (this option can be used more than once)\&. 45 | .RE 46 | .PP 47 | \fB\-c, \-\-config\fR=\fICONFIG\fR 48 | .RS 4 49 | Use the configuration file 50 | \fICONFIG\fR, otherwise pycardsyncer will look in 51 | \fI$HOME/\&.pycard/\fR 52 | and 53 | \fI$HOME/\&.config/pycard/\fR 54 | for files named 55 | \fIpycard\&.conf\fR\&. 56 | .RE 57 | .PP 58 | \fB\-\-debug\fR 59 | .RS 4 60 | This option enables debugging output\&. 61 | .RE 62 | .PP 63 | \fB\-h, \-\-help\fR 64 | .RS 4 65 | Print a small help text and exit\&. 66 | .RE 67 | .PP 68 | \fB\-v, \-\-version\fR 69 | .RS 4 70 | Print pycardsyncer\(cqs version number and exit\&. 71 | .RE 72 | .SH "AUTHOR" 73 | .sp 74 | pycardsyncer was mostly written by Christian Geier, with a lot of help by others, see \fICONTRIBUTORS\&.txt\fR in the pycarddav distribution\&. 75 | .SH "RESOURCES" 76 | .sp 77 | 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/\&. 78 | .SH "SEE ALSO" 79 | .sp 80 | pc_query(1), pycard\-import(1) 81 | .SH "COPYING" 82 | .sp 83 | 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\&. 84 | -------------------------------------------------------------------------------- /doc/man/pycardsyncer.txt: -------------------------------------------------------------------------------- 1 | PYCARDSYNCER(1) 2 | =============== 3 | :doctype: manpage 4 | 5 | NAME 6 | ---- 7 | pycardsyncer - synchronizes CardDAV resources to local db 8 | 9 | 10 | SYNOPSIS 11 | -------- 12 | *pycardsyncer* ['OPTIONS'] 13 | 14 | 15 | DESCRIPTION 16 | ----------- 17 | pycardsyncer(1) synchronizes remote CardDAV resources to a local database that can 18 | then be queried and modified with pc_query(1). 19 | 20 | 21 | OPTIONS 22 | ------- 23 | *-a, --account*='NAME':: 24 | Sync only the account/resource 'NAME' (this option can be used more than 25 | once). 26 | *-c, --config*='CONFIG':: 27 | Use the configuration file 'CONFIG', otherwise pycardsyncer will look in 28 | '$HOME/.pycard/' and '$HOME/.config/pycard/' for files named 'pycard.conf'. 29 | *--debug*:: 30 | This option enables debugging output. 31 | *-h, --help*:: 32 | Print a small help text and exit. 33 | *-v, --version*:: 34 | Print pycardsyncer's version number and exit. 35 | 36 | 37 | AUTHOR 38 | ------ 39 | pycardsyncer was mostly written by Christian Geier, with a lot of help by 40 | others, see 'CONTRIBUTORS.txt' in the pycarddav distribution. 41 | 42 | 43 | RESOURCES 44 | --------- 45 | Main web site: 46 | Please report bugs via the contact information at the above web site or 47 | via github: . 48 | 49 | 50 | SEE ALSO 51 | -------- 52 | pc_query(1), pycard-import(1) 53 | 54 | 55 | COPYING 56 | ------- 57 | Copyright \(C) 2011-2014 Christian Geier and Contributors. 58 | pycardsyncer and pycarddav are released under the terms of the Expat/MIT 59 | license, see the 'COPYING' file distributed with pycarddav. 60 | -------------------------------------------------------------------------------- /doc/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ----- 3 | *pyCardDAV* consists of three scripts, *pycardsyncer* which is used to sync the 4 | local database with the server, *pc_query* to interact with the local database 5 | and *pycard-import* to import email addresses from mutt. 6 | 7 | Execute pycardsyncer to sync your addresses to the local database. You can test 8 | pc_query with:: 9 | 10 | % pc_query searchstring 11 | 12 | By default *pyCardDAV* only prints the names, email addresses and telephone 13 | numbers of contacts matching the search string, to see all vCard properties use 14 | the "-A" option. 15 | 16 | 17 | For usage with mutt etc., *pyCardDAV* can also print only email addresses in a 18 | mutt friendly format (with the "-m" option). Edit your mutt configuration so 19 | that query_command uses pc_query: 20 | 21 | Example from .muttrc:: 22 | 23 | set query_command="/home/username/bin/pc_query -m %s" 24 | 25 | The current version features experimental write support. If you want to 26 | test this, first make sure **you have a backup of your data** (but please do 27 | *NOT* rely on *pc_query --backup* for this just yet), then you can put the 28 | line:: 29 | 30 | write_support = YesPleaseIDoHaveABackupOfMyData 31 | 32 | in your config file (needs to be put into each *Account* section you want to 33 | enable write support for). 34 | 35 | You can also import, delete or backup single cards (backup also works for the 36 | whole collection, but please don't rely on it just yet). See *pc_query --help* 37 | for how to use these and for some more options. 38 | 39 | *pycarddav* can be configured to use different CardDAV accounts, see the example 40 | config for details. An account can be specified with *-a account_name* with all 41 | three utilies. If no account is chosen all searching and syncing actions will 42 | use all configured accounts, while on adding cards the first configured account 43 | will be used. 44 | 45 | Keyring support 46 | --------------- 47 | 48 | *pycarddav* supports keyring_, (version >=3.0). To use it, you need to add a 49 | password to the keyring via:: 50 | 51 | keyring set pycarddav:$account $username 52 | 53 | where $account is the name of an account as configured in your configuration 54 | file and $username is the corresponding username (and then have no password 55 | configured for that account). For more details on configuring keyring have a 56 | look at its documentation_. 57 | 58 | .. _keyring: https://pypi.python.org/pypi/keyring 59 | .. _documentation: https://pypi.python.org/pypi/keyring 60 | 61 | Import Addresses from Mutt 62 | -------------------------- 63 | You can directly add sender addresses from mutt to *pyCardDAV*, either adding 64 | them to existing contacts or creating a new one. If write support is enabled, 65 | they will be uploaded on the server during the next sync. 66 | 67 | Example from .muttrc:: 68 | 69 | macro index,pager A "pycard-import" "add sender address to pycardsyncer" 70 | 71 | SSL 72 | --- 73 | If you use SSL to interact with your CardDAV Server (you probably should) and 74 | you don't have a certificate signed by a CA your OS Vendor trusts (like a 75 | self-signed certificate or one signed by CAcert) you can set *verify* to a path 76 | to the CA's root file (must be in pem format). If you don't want any certificate 77 | checking set *verify* to *false* to disable *any* ssl certificate checking (this 78 | is not recommended). 79 | 80 | Conflict Resolution 81 | ------------------- 82 | 83 | In case of conflicting edits (local VCard changed while remote VCard also 84 | changed), are "resolved" by pycarddav through overwriting the local VCard with 85 | the remote one (meaning local edits are lost in this case). Syncing more 86 | frequently can prevent this. 87 | 88 | Additional Information 89 | ---------------------- 90 | For now, VCard properties that have no value are not shown. 91 | 92 | Also, you should be able to use *pyCardDAV*'s CardDAV implementation for other 93 | projects. See the *CardDAV* class in *pycarddav/carddav.py*. 94 | 95 | In accordance with RFC 6352 all VCards that are imported or changed by pyCardDAV 96 | will automatically get a random UID (if they haven't one already), as some 97 | CardDAV servers, e.g. Owncloud require these. 98 | 99 | 100 | -------------------------------------------------------------------------------- /pycard.conf.sample: -------------------------------------------------------------------------------- 1 | [Account work] 2 | # DAV credentials. Please note that the password is written in plain 3 | # text here. If the password is missing, it will be claimed at 4 | # synchronization time. 5 | user: username 6 | passwd: yourpassword 7 | 8 | # A shell command line to read the password. 9 | # This can be used in instead of passwd to read a password from the standard output 10 | # of another program (e.g. a password manager). If passwd is set, this is ignored. 11 | #passwd_cmd: pass show carddav 12 | 13 | # The path to the CardDAV resource. 14 | #resource: https://[server]/owncloud/apps/contacts/carddav.php/addressbooks/[user]/[addressbook name]/ 15 | resource: https://carddav.server.tld:443/davical/caldav.php/username/addresses/ 16 | 17 | # Authentication Method: possible values are: basic (the default), or digest 18 | # (for servers that need HTTP digest authentification) 19 | #auth: basic 20 | 21 | # If verify is set to False, no SSL Certificate checks are done at all. Please 22 | # be aware of the security implications. The default value is True You can also 23 | # set verify to a path to your CAcert file 24 | #verify: True 25 | 26 | [Account private] 27 | user: anothername 28 | passwd: otherPasswd 29 | 30 | resource: https://domain.tld/contacts/addressbook/ 31 | 32 | [sqlite] 33 | # The location of the local SQLite contacts database. 34 | # Defaults to $XDG_DATA_HOME/pycard/abook.db 35 | #path: ~/.pycard/abook.db 36 | 37 | [query] 38 | # select where to search when querying, possible values are: 39 | # vcard, name, fname or allnames (which includes both the name and the full 40 | # name). Default is vcard, which search in all the fields but is slower. 41 | where: vcard 42 | 43 | [default] 44 | debug: False 45 | -------------------------------------------------------------------------------- /pycarddav/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # vim: set fileencoding=utf-8 : 3 | # Copyright (c) 2011-2014 Christian Geier & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | import argparse 25 | import ConfigParser 26 | import getpass 27 | import re 28 | import logging 29 | import os 30 | import signal 31 | import subprocess 32 | import sys 33 | import xdg.BaseDirectory 34 | 35 | import version 36 | 37 | from netrc import netrc 38 | from urlparse import urlsplit 39 | 40 | __productname__ = 'pyCardDAV' 41 | __version__ = version.__version__ 42 | __author__ = 'Christian Geier' 43 | __copyright__ = 'Copyright 2011-2013 Christian Geier & contributors' 44 | __author_email__ = 'pycarddav@lostpackets.de' 45 | __description__ = 'A CardDAV based address book tool' 46 | __license__ = 'Expat/MIT, see COPYING' 47 | __homepage__ = 'http://lostpackets.de/pycarddav/' 48 | 49 | 50 | def capture_user_interruption(): 51 | """ 52 | Tries to hide to the user the ugly python backtraces generated by 53 | pressing Ctrl-C. 54 | """ 55 | signal.signal(signal.SIGINT, lambda x, y: sys.exit(0)) 56 | 57 | 58 | class Namespace(dict): 59 | """The pycarddav configuration holder. 60 | 61 | This holder is a dict subclass that exposes its items as attributes. 62 | Inspired by NameSpace from argparse, Configuration is a simple 63 | object providing equality by attribute names and values, and a 64 | representation. 65 | 66 | Warning: Namespace instances do not have direct access to the dict 67 | methods. But since it is a dict object, it is possible to call 68 | these methods the following way: dict.get(ns, 'key') 69 | 70 | See http://code.activestate.com/recipes/577887-a-simple-namespace-class/ 71 | """ 72 | def __init__(self, obj=None): 73 | dict.__init__(self, obj if obj else {}) 74 | 75 | def __dir__(self): 76 | return list(self) 77 | 78 | def __repr__(self): 79 | return "%s(%s)" % (type(self).__name__, dict.__repr__(self)) 80 | 81 | def __getattribute__(self, name): 82 | try: 83 | return self[name] 84 | except KeyError: 85 | msg = "'%s' object has no attribute '%s'" 86 | raise AttributeError(msg % (type(self).__name__, name)) 87 | 88 | def __setattr__(self, name, value): 89 | self[name] = value 90 | 91 | def __delattr__(self, name): 92 | del self[name] 93 | 94 | 95 | class Section(object): 96 | 97 | READERS = {bool: ConfigParser.SafeConfigParser.getboolean, 98 | float: ConfigParser.SafeConfigParser.getfloat, 99 | int: ConfigParser.SafeConfigParser.getint, 100 | str: ConfigParser.SafeConfigParser.get} 101 | 102 | def __init__(self, parser, group): 103 | self._parser = parser 104 | self._group = group 105 | self._schema = None 106 | self._parsed = {} 107 | 108 | def matches(self, name): 109 | return self._group == name.lower() 110 | 111 | def is_collection(self): 112 | return False 113 | 114 | def parse(self, section): 115 | if self._schema is None: 116 | return None 117 | 118 | for option, default, filter_ in self._schema: 119 | try: 120 | if filter_ is None: 121 | reader = ConfigParser.SafeConfigParser.get 122 | filter_ = lambda x: x 123 | else: 124 | reader = Section.READERS[type(default)] 125 | self._parsed[option] = filter_(reader(self._parser, section, option)) 126 | # Remove option once handled (see the check function). 127 | self._parser.remove_option(section, option) 128 | except ConfigParser.Error: 129 | if filter_ is None: 130 | self._parsed[option] = default 131 | else: 132 | self._parsed[option] = filter_(default) 133 | 134 | return Namespace(self._parsed) 135 | 136 | @property 137 | def group(self): 138 | return self._group 139 | 140 | def _parse_verify(self, value): 141 | """if value is either 'True' or 'False' it returns that value as a bool, 142 | otherwise it returns the value""" 143 | boolvalue = value.strip().lower() 144 | if boolvalue == 'true': 145 | return True 146 | elif boolvalue == 'false': 147 | return False 148 | else: 149 | return os.path.expanduser(value) 150 | 151 | def _parse_write_support(self, value): 152 | """returns True if value is YesPlease..., this is a rather dirty 153 | solution, but it works fine (TM)""" 154 | value = value.strip() 155 | if value == 'YesPleaseIDoHaveABackupOfMyData': 156 | return True 157 | else: 158 | return False 159 | 160 | 161 | class AccountSection(Section): 162 | def __init__(self, parser): 163 | Section.__init__(self, parser, 'accounts') 164 | self._schema = [ 165 | ('user', '', None), 166 | ('passwd', '', None), 167 | ('passwd_cmd', '', None), 168 | ('resource', '', None), 169 | ('auth', 'basic', None), 170 | ('verify', 'true', self._parse_verify), 171 | ('write_support', '', self._parse_write_support), 172 | ] 173 | 174 | def is_collection(self): 175 | return True 176 | 177 | def matches(self, name): 178 | match = re.match('account (?P.*)', name, re.I) 179 | if match: 180 | self._parsed['name'] = match.group('name') 181 | return match is not None 182 | 183 | 184 | class SQLiteSection(Section): 185 | def __init__(self, parser): 186 | Section.__init__(self, parser, 'sqlite') 187 | self._schema = [ 188 | ('path', ConfigurationParser.DEFAULT_DB_PATH, os.path.expanduser), 189 | ] 190 | 191 | 192 | class ConfigurationParser(object): 193 | """A Configuration setup tool. 194 | 195 | This object takes care of command line parsing as well as 196 | configuration loading. It also prepares logging and updates its 197 | output level using the debug flag read from the command-line or 198 | the configuration file. 199 | """ 200 | DEFAULT_DB_PATH = xdg.BaseDirectory.save_data_path('pycard') + '/abook.db' 201 | DEFAULT_PATH = "pycard" 202 | DEFAULT_FILE = "pycard.conf" 203 | 204 | def __init__(self, desc, check_accounts=True): 205 | # Set the configuration current schema. 206 | self._sections = [AccountSection, SQLiteSection] 207 | 208 | # Build parsers and set common options. 209 | self._check_accounts = check_accounts 210 | self._conf_parser = ConfigParser.SafeConfigParser() 211 | self._arg_parser = argparse.ArgumentParser(description=desc) 212 | self._arg_parser.add_argument( 213 | "-v", "--version", action="version", version=__version__) 214 | self._arg_parser.add_argument( 215 | "-c", "--config", action="store", dest="filename", 216 | default=self._get_default_configuration_file(), metavar="FILE", 217 | help="an alternate configuration file") 218 | self._arg_parser.add_argument( 219 | "--debug", action="store_true", dest="debug", help="enables debugging") 220 | 221 | def parse(self): 222 | """Start parsing. 223 | 224 | Once the commandline parser is eventually configured with specific 225 | options, this function must be called to start parsing. It first 226 | parses the command line, and then the configuration file. 227 | 228 | If parsing is successful, the function check is then called. 229 | When check is a success, the Configuration instance is 230 | returned. On any error, None is returned. 231 | """ 232 | args = self._read_command_line() 233 | 234 | # Prepare the logger with the level read from command line. 235 | logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) 236 | 237 | if not args.filename: 238 | logging.error('Could not find configuration file') 239 | return None 240 | try: 241 | if not self._conf_parser.read(os.path.expanduser(args.filename)): 242 | logging.error('Cannot read %s', args.filename) 243 | return None 244 | else: 245 | logging.debug('Using configuration from %s', args.filename) 246 | except ConfigParser.Error, e: 247 | logging.error("Could not parse %s: %s", args.filename, e) 248 | return None 249 | 250 | conf = self._read_configuration(args) 251 | 252 | # Update the logger using the definitive output level. 253 | logging.getLogger().setLevel(logging.DEBUG if conf.debug else logging.INFO) 254 | 255 | return conf if self.check(conf) else None 256 | 257 | def check(self, ns): 258 | """Check the configuration before returning it from parsing. 259 | 260 | This default implementation warns the user of the remaining 261 | options found in the configuration file. It then checks the 262 | validity of the common configuration values. It returns True 263 | on success, False otherwise. 264 | 265 | This function can be overriden to augment the checks or the 266 | configuration tweaks achieved before the parsing function 267 | returns. 268 | """ 269 | result = True 270 | 271 | for section in self._conf_parser.sections(): 272 | for option in self._conf_parser.options(section): 273 | logging.debug("Ignoring %s:%s in configuration file", section, option) 274 | 275 | if self._check_accounts: 276 | if self.check_property(ns, 'accounts'): 277 | for account in ns.accounts: 278 | result &= self.check_account(account) 279 | else: 280 | logging.error("No account found") 281 | result = False 282 | 283 | # create the db dir if it doesn't exist 284 | dbdir = ns.sqlite.path.rsplit('/', 1)[0] 285 | if not os.path.isdir(dbdir): 286 | try: 287 | logging.debug('trying to create the directory for the db') 288 | os.makedirs(dbdir, mode=0770) 289 | logging.debug('success') 290 | except OSError as error: 291 | logging.fatal('failed to create {0}: {1}'.format(dbdir, error)) 292 | return False 293 | 294 | return result 295 | 296 | def check_account(self, ns): 297 | result = True 298 | 299 | if not ns.auth in ['basic', 'digest']: 300 | logging.error("Value %s is not allowed for in Account %s:auth", 301 | ns.auth, ns.name) 302 | result = False 303 | 304 | if not self.check_property(ns, 'resource', 'Account %s:resource' % ns.name): 305 | return False 306 | 307 | if ns.resource[-1] != '/': 308 | ns.resource = ns.resource + '/' 309 | 310 | 311 | if not len(ns.passwd): 312 | hostname = urlsplit(ns.resource).hostname 313 | try: 314 | auths = netrc().authenticators(hostname) 315 | except IOError: 316 | auths = False 317 | if auths: 318 | if not ns.user or auths[0] == ns.user: 319 | logging.debug("Read password for user %s on %s in .netrc", 320 | auths[0], hostname) 321 | ns.user = auths[0] 322 | ns.passwd = auths[2] 323 | else: 324 | logging.error("User %s not found for %s in .netrc", 325 | ns.user, hostname) 326 | result = False 327 | elif ns.user and ns.passwd_cmd: 328 | try: 329 | ns.passwd = subprocess.check_output(ns.passwd_cmd.split()) 330 | ns.passwd = ns.passwd.strip() # strip trailing newline 331 | except (OSError, subprocess.CalledProcessError) as e: 332 | logging.error("Failed to execute passwd command '%s'. Reason: %s", 333 | ns.passwd_cmd, e) 334 | result = False 335 | elif ns.user: 336 | try: 337 | import keyring 338 | except ImportError: 339 | pass 340 | else: 341 | ns.passwd = keyring.get_password('pycarddav:'+ns.name, ns.user) 342 | # Do not ask for password if execution is already doomed. 343 | if result and not ns.passwd: 344 | prompt = 'CardDAV password (account ' + ns.name + '): ' 345 | ns.passwd = getpass.getpass(prompt=prompt) 346 | else: 347 | logging.error("Missing credentials for %s", hostname) 348 | result = False 349 | 350 | return result 351 | 352 | def check_property(self, ns, property_, display_name=None): 353 | names = property_.split('.') 354 | obj = ns 355 | try: 356 | for name in names: 357 | obj = dict.get(obj, name) 358 | if not obj: 359 | raise AttributeError() 360 | except AttributeError: 361 | logging.fatal('Mandatory option %s is missing', 362 | display_name if display_name else property_) 363 | return False 364 | 365 | return True 366 | 367 | def dump(self, conf, intro='Using configuration:', tab=1): 368 | """Dump the loaded configuration using the logging framework. 369 | 370 | The values displayed here are the exact values which are seen by 371 | the program, and not the raw values as they are read in the 372 | configuration file. 373 | """ 374 | logging.debug(intro) 375 | 376 | for name, value in sorted(dict.copy(conf).iteritems()): 377 | if type(value) is list and not isinstance(value[0], basestring): 378 | for o in value: 379 | self.dump(o, '\t' * tab + name + ':', tab + 1) 380 | elif type(value) is Namespace: 381 | self.dump(value, '\t' * tab + name + ':', tab + 1) 382 | elif name != 'passwd': 383 | logging.debug('%s%s: %s', '\t'*tab, name, value) 384 | 385 | def _read_command_line(self): 386 | items = {} 387 | for key, value in vars(self._arg_parser.parse_args()).iteritems(): 388 | if '__' in key: 389 | section, option = key.split('__') 390 | items.setdefault(section, Namespace({}))[option] = value 391 | else: 392 | items[key] = value 393 | return Namespace(items) 394 | 395 | def _read_configuration(self, overrides): 396 | """Build the configuration holder. 397 | 398 | First, data declared in the configuration schema are extracted 399 | from the configuration file, with type checking and possibly 400 | through a filter. Then these data are completed or overriden 401 | using the values read from the command line. 402 | """ 403 | items = {} 404 | try: 405 | if self._conf_parser.getboolean('default', 'debug'): 406 | overrides['debug'] = True 407 | except ValueError: 408 | pass 409 | 410 | for section in self._conf_parser.sections(): 411 | parser = self._get_section_parser(section) 412 | if not parser is None: 413 | values = parser.parse(section) 414 | if parser.is_collection(): 415 | if not items.has_key(parser.group): 416 | items[parser.group] = [] 417 | items[parser.group].append(values) 418 | else: 419 | items[parser.group] = values 420 | 421 | for key in dir(overrides): 422 | items[key] = Namespace.get(overrides, key) 423 | 424 | return Namespace(items) 425 | 426 | def _get_section_parser(self, section): 427 | for cls in self._sections: 428 | parser = cls(self._conf_parser) 429 | if parser.matches(section): 430 | return parser 431 | return None 432 | 433 | def _get_default_configuration_file(self): 434 | """Return the configuration filename. 435 | 436 | This function builds the list of paths known by pycarddav and 437 | then return the first one which exists. The first paths 438 | searched are the ones described in the XDG Base Directory 439 | Standard. Each one of this path ends with 440 | DEFAULT_PATH/DEFAULT_FILE. 441 | 442 | On failure, the path DEFAULT_PATH/DEFAULT_FILE, prefixed with 443 | a dot, is searched in the home user directory. Ultimately, 444 | DEFAULT_FILE is searched in the current directory. 445 | """ 446 | paths = [] 447 | 448 | resource = os.path.join( 449 | ConfigurationParser.DEFAULT_PATH, ConfigurationParser.DEFAULT_FILE) 450 | paths.extend([os.path.join(path, resource) 451 | for path in xdg.BaseDirectory.xdg_config_dirs]) 452 | 453 | paths.append(os.path.expanduser(os.path.join('~', '.' + resource))) 454 | paths.append(os.path.expanduser(ConfigurationParser.DEFAULT_FILE)) 455 | 456 | for path in paths: 457 | if os.path.exists(path): 458 | return path 459 | 460 | return None 461 | 462 | 463 | class SyncConfigurationParser(ConfigurationParser): 464 | """A specialized setup tool for synchronization.""" 465 | def __init__(self): 466 | ConfigurationParser.__init__(self, "syncs the local db to the CardDAV server") 467 | 468 | self._arg_parser.add_argument( 469 | "-a", "--account", action="append", dest="sync__accounts", 470 | metavar="NAME", help="use only the NAME account (can be used more than once)") 471 | 472 | def check(self, ns): 473 | result = ConfigurationParser.check(self, ns) 474 | 475 | accounts = [account.name for account in ns.accounts] 476 | 477 | if ns.sync.accounts: 478 | for name in set(ns.sync.accounts): 479 | if not name in [a.name for a in ns.accounts]: 480 | logging.warn('Unknown account %s', name) 481 | ns.sync.accounts.remove(name) 482 | if len(ns.sync.accounts) == 0: 483 | logging.error('No valid account selected') 484 | result = False 485 | else: 486 | ns.sync.accounts = accounts 487 | 488 | ns.sync.accounts = set(ns.sync.accounts) 489 | 490 | return result 491 | -------------------------------------------------------------------------------- /pycarddav/backend.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # vim: set ts=4 sw=4 expandtab sts=4: 3 | # Copyright (c) 2011-2014 Christian Geier & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | """ 24 | The SQLite backend implementation. 25 | 26 | Database Layout 27 | =============== 28 | 29 | current version number: 9 30 | tables: version, accounts, account_$ACCOUNTNAME 31 | 32 | version: 33 | version (INT): only one line: current db version 34 | 35 | account: 36 | account (TEXT): name of the account 37 | resource (TEXT) 38 | ctag (TEXT) ctag of the collection 39 | 40 | $ACCOUNTNAME_r: # as in resource 41 | href (TEXT) 42 | etag (TEXT) 43 | name (TEXT): name as in vcard, seperated by ';' 44 | fname (TEXT): formated name 45 | status (INT): status of this card, see below for meaning 46 | vcard (TEXT): the actual vcard 47 | 48 | """ 49 | 50 | # TODO rename account to resource or similar 51 | 52 | from __future__ import print_function 53 | 54 | try: 55 | from pycarddav import model 56 | import xdg.BaseDirectory 57 | import sys 58 | import sqlite3 59 | import logging 60 | from os import path 61 | 62 | except ImportError, error: 63 | print(error) 64 | sys.exit(1) 65 | 66 | 67 | OK = 0 # not touched since last sync 68 | NEW = 1 # new card, needs to be created on the server 69 | CHANGED = 2 # properties edited or added (news to be pushed to server) 70 | DELETED = 9 # marked for deletion (needs to be deleted on server) 71 | 72 | 73 | class SQLiteDb(object): 74 | """Querying the addressbook database 75 | 76 | the type() of parameters named "account" should be something like str() 77 | and of parameters named "accountS" should be an iterable like list() 78 | """ 79 | 80 | def __init__(self, 81 | db_path=None, 82 | encoding="utf-8", 83 | errors="strict", 84 | debug=False): 85 | if db_path is None: 86 | db_path = xdg.BaseDirectory.save_data_path('pycard') + 'abook.db' 87 | self.db_path = path.expanduser(db_path) 88 | self.conn = sqlite3.connect(self.db_path) 89 | self.cursor = self.conn.cursor() 90 | self.encoding = encoding 91 | self.errors = errors 92 | self.debug = debug 93 | self.display_all = False 94 | self.print_function = "print_contact_info" 95 | self._create_default_tables() 96 | self._check_table_version() 97 | 98 | def __del__(self): 99 | self.conn.close() 100 | 101 | def search(self, search_string, accounts, where='vcard'): 102 | """returns list of parsed vcards from db matching search_string 103 | where can be any of 'vcard', 'name', 'fname' or 'allnames' (meaning is 104 | searched for both 'name' or 'fname' for matches) 105 | """ 106 | if where not in ('vcard', 'name', 'fname', 'allnames'): 107 | raise ValueError("Invalid 'where' argument") 108 | 109 | search_str = '%' + search_string + '%' 110 | sql_fmt = 'SELECT href, vcard, etag FROM {0} WHERE ' 111 | 112 | if where == 'allnames': 113 | sql_fmt += 'name LIKE (?) OR fname LIKE (?)' 114 | sql_args = (search_str, search_str) 115 | else: 116 | sql_fmt += where + ' LIKE (?)' 117 | sql_args = (search_str,) 118 | 119 | result = list() 120 | for account in accounts: 121 | rows = self.sql_ex(sql_fmt.format(account + '_r'), sql_args) 122 | result.extend((self.get_vcard_from_data(account, *r) for r in rows)) 123 | return result 124 | 125 | def _dump(self, account_name): 126 | """return table self.account, used for testing""" 127 | sql_s = 'SELECT * FROM {0}'.format(account_name + '_r') 128 | result = self.sql_ex(sql_s) 129 | return result 130 | 131 | def _check_table_version(self): 132 | """tests for current db Version 133 | if the table is still empty, insert db_version 134 | """ 135 | database_version = 11 # the current db VERSION 136 | self.cursor.execute('SELECT version FROM version') 137 | result = self.cursor.fetchone() 138 | if result is None: 139 | stuple = (database_version, ) # database version db Version 140 | self.cursor.execute('INSERT INTO version (version) VALUES (?)', 141 | stuple) 142 | self.conn.commit() 143 | elif not result[0] == database_version: 144 | raise Exception(str(self.db_path) + 145 | " is probably an invalid or outdated database.\n" 146 | "You should consider to remove it and sync again " 147 | "using pycardsyncer.\n") 148 | 149 | def _create_default_tables(self): 150 | """creates version and account tables and insert table version number 151 | 152 | """ 153 | # CREATE TABLE IF NOT EXISTS is faster than checking if it exists 154 | try: 155 | self.cursor.execute('''CREATE TABLE IF NOT EXISTS version 156 | ( version INTEGER )''') 157 | logging.debug("made sure version table exists") 158 | except Exception as error: 159 | sys.stderr.write('Failed to connect to database,' 160 | 'Unknown Error: ' + str(error) + "\n") 161 | self.conn.commit() 162 | try: 163 | self.cursor.execute('''CREATE TABLE IF NOT EXISTS accounts ( 164 | account TEXT NOT NULL, 165 | resource TEXT NOT NULL, 166 | ctag TEXT 167 | )''') 168 | logging.debug("made sure accounts table exists ") 169 | except Exception as error: 170 | sys.stderr.write('Failed to connect to database,' 171 | 'Unknown Error: ' + str(error) + "\n") 172 | self.conn.commit() 173 | self._check_table_version() # insert table version 174 | 175 | def sql_ex(self, statement, stuple=''): 176 | """wrapper for sql statements, does a "fetchall" """ 177 | self.cursor.execute(statement, stuple) 178 | result = self.cursor.fetchall() 179 | self.conn.commit() 180 | return result 181 | 182 | def check_account_table(self, account_name, resource): 183 | count_sql_s = """SELECT count(*) FROM accounts 184 | WHERE account = ? AND resource = ?""" 185 | self.cursor.execute(count_sql_s, (account_name, resource)) 186 | result = self.cursor.fetchone() 187 | 188 | if(result[0] != 0): 189 | return 190 | sql_s = """CREATE TABLE IF NOT EXISTS {0} ( 191 | href TEXT, 192 | etag TEXT, 193 | name TEXT, 194 | fname TEXT, 195 | vcard TEXT, 196 | status INT NOT NULL, 197 | PRIMARY KEY(href) 198 | )""".format(account_name + '_r') 199 | self.sql_ex(sql_s) 200 | sql_s = 'INSERT INTO accounts (account, resource) VALUES (?, ?)' 201 | self.sql_ex(sql_s, (account_name + '_r', resource)) 202 | logging.debug("made sure {0} table exists".format(account_name)) 203 | 204 | def needs_update(self, href, account_name, etag=''): 205 | """checks if we need to update this vcard 206 | if no table with the name account_$ACCOUNT exists, it will be created 207 | 208 | :param href: href of vcard 209 | :type href: str() 210 | :param etag: etag of vcard 211 | :type etag: str() 212 | :return: True or False 213 | """ 214 | stuple = (href,) 215 | sql_s = 'SELECT etag FROM {0} WHERE href = ?'.format(account_name + '_r') 216 | result = self.sql_ex(sql_s, stuple) 217 | if len(result) is 0: 218 | return True 219 | elif etag != result[0][0]: 220 | return True 221 | else: 222 | return False 223 | 224 | def update(self, vcard, account_name, href='', etag='', status=OK): 225 | """insert a new or update an existing card in the db 226 | 227 | :param vcard: vcard to be inserted or updated 228 | :type vcard: model.VCard() or unicode() (an actual vcard) 229 | :param href: href of the card on the server, if this href already 230 | exists in the db the card gets updated. If no href is 231 | given, a random href is chosen and it is implied that this 232 | card does not yet exist on the server, but will be 233 | uploaded there on next sync. 234 | :type href: str() 235 | :param etag: the etga of the vcard, if this etag does not match the 236 | remote etag on next sync, this card will be updated from 237 | the server. For locally created vcards this should not be 238 | set 239 | :type etag: str() 240 | :param status: status of the vcard 241 | * OK: card is in sync with remote server 242 | * NEW: card is not yet on the server, this needs to be 243 | set for locally created vcards 244 | * CHANGED: card locally changed, will be updated on the 245 | server on next sync (if remote card has not 246 | changed since last sync) 247 | * DELETED: card locally delete, will also be deleted on 248 | one the server on next sync (if remote card 249 | has not changed) 250 | :type status: one of backend.OK, backend.NEW, backend.CHANGED, 251 | BACKEND.DELETED 252 | 253 | """ 254 | if isinstance(vcard, (str, unicode)): # unicode for py2, str for py3 255 | try: 256 | vcard_s = vcard.decode('utf-8') 257 | except UnicodeEncodeError: 258 | vcard_s = vcard # incase it's already unicode and py2 259 | try: 260 | vcard = model.vcard_from_string(vcard) 261 | except Exception as error: 262 | logging.error('VCard {0} could not be inserted into the ' 263 | 'db'.format(href)) 264 | logging.debug(error) 265 | logging.info(vcard) 266 | return 267 | else: 268 | vcard_s = vcard.vcf 269 | if href == '': 270 | href = get_random_href() 271 | stuple = (etag, vcard.name, vcard.fname, vcard_s, status, href, href) 272 | sql_s = ('INSERT OR REPLACE INTO {0} ' 273 | '(etag, name, fname, vcard, status, href) ' 274 | 'VALUES (?, ?, ?, ?, ?, ' 275 | 'COALESCE((SELECT href FROM {0} WHERE href = ?), ?)' 276 | ');'.format(account_name + '_r')) 277 | self.sql_ex(sql_s, stuple) 278 | 279 | def update_href(self, old_href, new_href, account_name, etag='', status=OK): 280 | """updates old_href to new_href, can also alter etag and status, 281 | see update() for an explanation of these parameters""" 282 | stuple = (new_href, etag, status, old_href) 283 | sql_s = 'UPDATE {0} SET href = ?, etag = ?, status = ? \ 284 | WHERE href = ?;'.format(account_name + '_r') 285 | self.sql_ex(sql_s, stuple) 286 | 287 | def href_exists(self, href, account_name): 288 | """returns True if href already exist in db 289 | 290 | :param href: href 291 | :type href: str() 292 | :returns: True or False 293 | """ 294 | sql_s = 'SELECT href FROM {0} WHERE href = ?;'.format(account_name + '_r') 295 | if len(self.sql_ex(sql_s, (href, ))) == 0: 296 | return False 297 | else: 298 | return True 299 | 300 | def get_etag(self, href, account_name): 301 | """get etag for href 302 | 303 | type href: str() 304 | return: etag 305 | rtype: str() 306 | """ 307 | sql_s = 'SELECT etag FROM {0} WHERE href=(?);'.format(account_name + '_r') 308 | etag = self.sql_ex(sql_s, (href,))[0][0] 309 | return etag 310 | 311 | def delete_vcard_from_db(self, href, account_name): 312 | """ 313 | removes the whole vcard, 314 | returns nothing 315 | """ 316 | stuple = (href, ) 317 | logging.debug("locally deleting " + str(href)) 318 | self.sql_ex('DELETE FROM {0} WHERE href=(?)'.format(account_name + '_r'), 319 | stuple) 320 | 321 | def get_all_href_from_db(self, accounts): 322 | """returns a list with all parsed vcards 323 | """ 324 | result = list() 325 | for account in accounts: 326 | rows = self.sql_ex( 327 | 'SELECT href, vcard, etag FROM {0} ORDER BY fname' 328 | ' COLLATE NOCASE'.format(account + '_r')) 329 | result.extend((self.get_vcard_from_data(account, *r) for r in rows)) 330 | return result 331 | 332 | def get_all_href_from_db_not_new(self, accounts): 333 | """returns list of all not new hrefs""" 334 | result = list() 335 | for account in accounts: 336 | sql_s = 'SELECT href FROM {0} WHERE status != (?)'.format(account + '_r') 337 | stuple = (NEW,) 338 | hrefs = self.sql_ex(sql_s, stuple) 339 | result = result + [(href[0], account) for href in hrefs] 340 | return result 341 | 342 | # def get_names_href_from_db(self, searchstring=None): 343 | # """ 344 | # :return: list of tuples(name, href) of all entries from the db 345 | # """ 346 | # if searchstring is None: 347 | # return self.sql_ex('SELECT fname, href FROM {0} ' 348 | # 'ORDER BY name'.format(self.account)) 349 | # else: 350 | # return [(c.fname, c.href) for c in self.search(searchstring)] 351 | 352 | def get_vcard_from_data(self, account_name, href, vcard, etag): 353 | """returns a VCard() 354 | """ 355 | vcard = model.vcard_from_string(vcard) 356 | vcard.href = href 357 | vcard.account = account_name 358 | vcard.etag = etag 359 | return vcard 360 | 361 | def get_vcard_from_db(self, href, account_name): 362 | """returns a VCard() 363 | """ 364 | sql_s = 'SELECT vcard, etag FROM {0} WHERE href=(?)'.format(account_name + '_r') 365 | result = self.sql_ex(sql_s, (href, )) 366 | return self.get_vcard_from_data(account_name, href, *result[0]) 367 | 368 | def get_changed(self, account_name): 369 | """returns list of hrefs of locally edited vcards 370 | """ 371 | sql_s = 'SELECT href FROM {0} WHERE status == (?)'.format(account_name + '_r') 372 | result = self.sql_ex(sql_s, (CHANGED, )) 373 | return [row[0] for row in result] 374 | 375 | def get_new(self, account_name): 376 | """returns list of hrefs of locally added vcards 377 | """ 378 | sql_s = 'SELECT href FROM {0} WHERE status == (?)'.format(account_name + '_r') 379 | result = self.sql_ex(sql_s, (NEW, )) 380 | return [row[0] for row in result] 381 | 382 | def get_marked_delete(self, account_name): 383 | """returns list of tuples (hrefs, etags) of locally deleted vcards 384 | """ 385 | sql_s = 'SELECT href, etag FROM {0} WHERE status == (?)'.format(account_name + '_r') 386 | result = self.sql_ex(sql_s, (DELETED, )) 387 | return result 388 | 389 | def mark_delete(self, href, account_name): 390 | """marks the entry as to be deleted on server on next sync 391 | """ 392 | sql_s = 'UPDATE {0} SET STATUS = ? WHERE href = ?'.format(account_name + '_r') 393 | self.sql_ex(sql_s, (DELETED, href, )) 394 | 395 | def reset_flag(self, href, account_name): 396 | """ 397 | resets the status for a given href to 0 (=not edited locally) 398 | """ 399 | sql_s = 'UPDATE {0} SET status = ? WHERE href = ?'.format(account_name + '_r') 400 | self.sql_ex(sql_s, (OK, href, )) 401 | 402 | 403 | def get_random_href(): 404 | """returns a random href 405 | """ 406 | import random 407 | tmp_list = list() 408 | for _ in xrange(3): 409 | rand_number = random.randint(0, 0x100000000) 410 | tmp_list.append("{0:x}".format(rand_number)) 411 | return "-".join(tmp_list).upper() 412 | -------------------------------------------------------------------------------- /pycarddav/carddav.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # vim: set ts=4 sw=4 expandtab sts=4: 3 | # Copyright (c) 2011-2014 Christian Geier & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | """ 24 | contains the class PyCardDAv and some associated functions and definitions 25 | """ 26 | 27 | from collections import namedtuple 28 | import requests 29 | import urlparse 30 | import logging 31 | import lxml.etree as ET 32 | 33 | 34 | def get_random_href(): 35 | """returns a random href""" 36 | import random 37 | tmp_list = list() 38 | for _ in xrange(3): 39 | rand_number = random.randint(0, 0x100000000) 40 | tmp_list.append("{0:x}".format(rand_number)) 41 | return "-".join(tmp_list).upper() 42 | 43 | 44 | class UploadFailed(Exception): 45 | """uploading the card failed""" 46 | pass 47 | 48 | 49 | class NoWriteSupport(Exception): 50 | """write support has not been enabled""" 51 | pass 52 | 53 | 54 | class PyCardDAV(object): 55 | """class for interacting with a CardDAV server 56 | 57 | Since PyCardDAV relies heavily on Requests [1] its SSL verification is also 58 | shared by PyCardDAV [2]. For now, only the *verify* keyword is exposed 59 | through PyCardDAV. 60 | 61 | [1] http://docs.python-requests.org/ 62 | [2] http://docs.python-requests.org/en/latest/user/advanced/ 63 | 64 | raises: 65 | requests.exceptions.SSLError 66 | requests.exceptions.ConnectionError 67 | more requests.exceptions depending on the actual error 68 | Exception (shame on me) 69 | 70 | """ 71 | 72 | def __init__(self, resource, debug='', user='', passwd='', 73 | verify=True, write_support=False, auth='basic'): 74 | #shutup urllib3 75 | urllog = logging.getLogger('requests.packages.urllib3.connectionpool') 76 | urllog.setLevel(logging.CRITICAL) 77 | urllog = logging.getLogger('urllib3.connectionpool') 78 | urllog.setLevel(logging.CRITICAL) 79 | 80 | # activate pyopenssl if available 81 | try: 82 | import urllib3.contrib.pyopenssl 83 | except ImportError: 84 | pass 85 | else: 86 | urllib3.contrib.pyopenssl.inject_into_urllib3() 87 | 88 | split_url = urlparse.urlparse(resource) 89 | url_tuple = namedtuple('url', 'resource base path') 90 | self.url = url_tuple(resource, 91 | split_url.scheme + '://' + split_url.netloc, 92 | split_url.path) 93 | self.debug = debug 94 | self.session = requests.session() 95 | self.write_support = write_support 96 | self._settings = {'verify': verify} 97 | if auth == 'basic': 98 | self._settings['auth'] = (user, passwd,) 99 | if auth == 'digest': 100 | from requests.auth import HTTPDigestAuth 101 | self._settings['auth'] = HTTPDigestAuth(user, passwd) 102 | self._default_headers = {"User-Agent": "pyCardDAV"} 103 | 104 | headers = self.headers 105 | headers['Depth'] = '1' 106 | response = self.session.request('OPTIONS', 107 | self.url.resource, 108 | headers=headers, 109 | **self._settings) 110 | response.raise_for_status() # raises error on not 2XX HTTP status code 111 | if 'addressbook' not in response.headers.get('DAV', ''): 112 | raise Exception("URL is not a CardDAV resource") 113 | 114 | @property 115 | def verify(self): 116 | """gets verify from settings dict""" 117 | return self._settings['verify'] 118 | 119 | @verify.setter 120 | def verify(self, verify): 121 | """set verify""" 122 | self._settings['verify'] = verify 123 | 124 | @property 125 | def headers(self): 126 | """returns the headers""" 127 | return dict(self._default_headers) 128 | 129 | def _check_write_support(self): 130 | """checks if user really wants his data destroyed""" 131 | if not self.write_support: 132 | raise NoWriteSupport 133 | 134 | def get_abook(self): 135 | """does the propfind and processes what it returns 136 | 137 | :rtype: list of hrefs to vcards 138 | """ 139 | xml = self._get_xml_props() 140 | abook = self._process_xml_props(xml) 141 | return abook 142 | 143 | def get_vcard(self, href): 144 | """ 145 | pulls vcard from server 146 | 147 | :returns: vcard 148 | :rtype: string 149 | """ 150 | response = self.session.get(self.url.base + href, 151 | headers=self.headers, 152 | **self._settings) 153 | response.raise_for_status() 154 | return response.content 155 | 156 | def update_vcard(self, card, href, etag): 157 | """ 158 | pushes changed vcard to the server 159 | card: vcard as unicode string 160 | etag: str or None, if this is set to a string, card is only updated if 161 | remote etag matches. If etag = None the update is forced anyway 162 | """ 163 | # TODO what happens if etag does not match? 164 | self._check_write_support() 165 | remotepath = str(self.url.base + href) 166 | headers = self.headers 167 | headers['content-type'] = 'text/vcard' 168 | if etag is not None: 169 | headers['If-Match'] = etag 170 | self.session.put(remotepath, data=card, headers=headers, 171 | **self._settings) 172 | 173 | def delete_vcard(self, href, etag): 174 | """deletes vcard from server 175 | 176 | deletes the resource at href if etag matches, 177 | if etag=None delete anyway 178 | :param href: href of card to be deleted 179 | :type href: str() 180 | :param etag: etag of that card, if None card is always deleted 181 | :type href: str() 182 | :returns: nothing 183 | """ 184 | # TODO: what happens if etag does not match, url does not exist etc ? 185 | self._check_write_support() 186 | remotepath = str(self.url.base + href) 187 | headers = self.headers 188 | headers['content-type'] = 'text/vcard' 189 | if etag is not None: 190 | headers['If-Match'] = etag 191 | response = self.session.delete(remotepath, 192 | headers=headers, 193 | **self._settings) 194 | response.raise_for_status() 195 | 196 | def upload_new_card(self, card): 197 | """ 198 | upload new card to the server 199 | 200 | :param card: vcard to be uploaded 201 | :type card: unicode 202 | :rtype: tuple of string (path of the vcard on the server) and etag of 203 | new card (string or None) 204 | """ 205 | self._check_write_support() 206 | card = card.encode('utf-8') 207 | for _ in range(0, 5): 208 | rand_string = get_random_href() 209 | remotepath = str(self.url.resource + rand_string + ".vcf") 210 | headers = self.headers 211 | headers['content-type'] = 'text/vcard' # TODO perhaps this should 212 | # be set to the value this carddav server uses itself 213 | headers['If-None-Match'] = '*' 214 | response = requests.put(remotepath, data=card, headers=headers, 215 | **self._settings) 216 | if response.ok: 217 | parsed_url = urlparse.urlparse(remotepath) 218 | if 'etag' not in response.headers.keys() or response.headers['etag'] is None: 219 | etag = '' 220 | else: 221 | etag = response.headers['etag'] 222 | 223 | return (parsed_url.path, etag) 224 | response.raise_for_status() 225 | 226 | def _get_xml_props(self): 227 | """PROPFIND method 228 | 229 | gets the xml file with all vcard hrefs 230 | 231 | :rtype: str() (an xml file) 232 | """ 233 | headers = self.headers 234 | headers['Depth'] = '1' 235 | response = self.session.request('PROPFIND', 236 | self.url.resource, 237 | headers=headers, 238 | **self._settings) 239 | response.raise_for_status() 240 | 241 | return response.content 242 | 243 | @classmethod 244 | def _process_xml_props(cls, xml): 245 | """processes the xml from PROPFIND, listing all vcard hrefs 246 | 247 | :param xml: the xml file 248 | :type xml: str() 249 | :rtype: dict() key: href, value: etag 250 | """ 251 | namespace = "{DAV:}" 252 | 253 | element = ET.XML(xml) 254 | abook = dict() 255 | for response in element.iterchildren(): 256 | if (response.tag == namespace + "response"): 257 | href = "" 258 | etag = "" 259 | insert = False 260 | for refprop in response.iterchildren(): 261 | if (refprop.tag == namespace + "href"): 262 | href = refprop.text 263 | for prop in refprop.iterchildren(): 264 | for props in prop.iterchildren(): 265 | # different servers give different getcontenttypes: 266 | # e.g.: 267 | # "text/vcard" 268 | # "text/x-vcard" 269 | # "text/x-vcard; charset=utf-8" 270 | # "text/directory;profile=vCard" 271 | # "text/directory" 272 | # "text/vcard; charset=utf-8" CalendarServer 273 | if (props.tag == namespace + "getcontenttype" and 274 | props.text.split(';')[0].strip() in ['text/vcard', 'text/x-vcard']): 275 | insert = True 276 | if (props.tag == namespace + "resourcetype" and 277 | namespace + "collection" in [c.tag for c in props.iterchildren()]): 278 | insert = False 279 | break 280 | if (props.tag == namespace + "getetag"): 281 | etag = props.text 282 | if insert: 283 | abook[href] = etag 284 | return abook 285 | -------------------------------------------------------------------------------- /pycarddav/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # coding: utf-8 3 | # vim: set ts=4 sw=4 expandtab sts=4: 4 | # Copyright (c) 2011-2014 Christian Geier & contributors 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files (the 8 | # "Software"), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so, subject to 12 | # the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be 15 | # included in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | """ 25 | controllers for the different tools 26 | """ 27 | 28 | -------------------------------------------------------------------------------- /pycarddav/controllers/query.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # coding: utf-8 3 | # vim: set ts=4 sw=4 expandtab sts=4: 4 | # Copyright (c) 2011-2014 Christian Geier & contributors 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files (the 8 | # "Software"), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so, subject to 12 | # the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be 15 | # included in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | """ 25 | query the local db 26 | """ 27 | 28 | from pycarddav import backend 29 | 30 | from os import path 31 | 32 | import sys 33 | 34 | __all__ = [ 'query' ] 35 | 36 | def query(conf): 37 | # testing if the db exists 38 | if not path.exists(path.expanduser(conf.sqlite.path)): 39 | sys.exit(str(conf.sqlite.path) + " file does not exist, please sync" 40 | " with pycardsyncer first.") 41 | 42 | search_string = conf.query.search_string.decode("utf-8") 43 | 44 | my_dbtool = backend.SQLiteDb(db_path=path.expanduser(conf.sqlite.path), 45 | encoding="utf-8", 46 | errors="stricts", 47 | debug=False) 48 | 49 | if conf.query.importing: 50 | action = importing 51 | elif conf.query.backup: 52 | action = backup 53 | #elif conf.query.edit: 54 | # action = edit 55 | elif conf.query.delete: # mark a card for deletion 56 | action = delete 57 | else: 58 | action = search 59 | 60 | action(my_dbtool, search_string, conf) 61 | 62 | return 0 63 | 64 | 65 | def importing(my_dbtool, search_string, conf): 66 | from pycarddav import model 67 | cards = model.cards_from_file(conf.query.importing) 68 | for card in cards: 69 | my_dbtool.update(card, conf.sync.accounts[0], status=backend.NEW) 70 | 71 | def backup(my_dbtool, search_string, conf): 72 | with open(conf.query.backup, 'w') as vcf_file: 73 | if search_string == "": 74 | vcards = my_dbtool.get_all_href_from_db(conf.sync.accounts) 75 | else: 76 | vcards = my_dbtool.search(search_string, conf.sync.accounts, 77 | conf.query.where) 78 | for vcard in vcards: 79 | vcf_file.write(vcard.vcf.encode('utf-8')) 80 | vcf_file.write('\n') 81 | 82 | def edit(my_dbtool, search_string, conf): 83 | from pycarddav import ui 84 | names = my_dbtool.select_entry2(search_string) 85 | href = ui.select_entry(names) 86 | if href is None: 87 | sys.exit("Found no matching cards.") 88 | 89 | def delete(my_dbtool, search_string, conf): 90 | vcards = my_dbtool.search(search_string, conf.sync.accounts, 91 | conf.query.where) 92 | if len(vcards) is 0: 93 | sys.exit('Found no matching cards.') 94 | elif len(vcards) is 1: 95 | card = vcards[0] 96 | else: 97 | from pycarddav import ui 98 | href_account_list = [(c.href, c.account) for c in vcards] 99 | pane = ui.VCardChooserPane(my_dbtool, 100 | href_account_list=href_account_list) 101 | ui.start_pane(pane) 102 | card = pane._walker.selected_vcard 103 | if card.href in my_dbtool.get_new(card.account): 104 | # cards not yet on the server get deleted directly, otherwise we 105 | # will try to delete them on the server later (where they don't 106 | # exist) and this will raise an exception 107 | my_dbtool.delete_vcard_from_db(card.href, card.account) 108 | else: 109 | my_dbtool.mark_delete(card.href, card.account) 110 | print(u'vcard {0} - "{1}" deleted from local db, ' 111 | 'will be deleted on the server on the next ' 112 | 'sync'.format(card.href, card.fname)) 113 | 114 | def search(my_dbtool, search_string, conf): 115 | print("searching for " + conf.query.search_string + "...") 116 | 117 | for vcard in my_dbtool.search(search_string, conf.sync.accounts, 118 | conf.query.where): 119 | if conf.query.mutt_format: 120 | lines = vcard.print_email() 121 | elif conf.query.tel: 122 | lines = vcard.print_tel() 123 | elif conf.query.display_all: 124 | lines = vcard.pretty 125 | else: 126 | lines = vcard.pretty_min 127 | if not lines == '': 128 | print(lines.encode('utf-8')) 129 | 130 | -------------------------------------------------------------------------------- /pycarddav/controllers/sync.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # coding: utf-8 3 | # vim: set ts=4 sw=4 expandtab sts=4: 4 | # Copyright (c) 2011-2014 Christian Geier & contributors 5 | # 6 | # Permission is hereby granted, free of charge, to any person obtaining 7 | # a copy of this software and associated documentation files (the 8 | # "Software"), to deal in the Software without restriction, including 9 | # without limitation the rights to use, copy, modify, merge, publish, 10 | # distribute, sublicense, and/or sell copies of the Software, and to 11 | # permit persons to whom the Software is furnished to do so, subject to 12 | # the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be 15 | # included in all copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | """ 25 | syncs the remote database to the local db 26 | """ 27 | 28 | from pycarddav import carddav 29 | from pycarddav import backend 30 | 31 | import logging 32 | 33 | __all__ = ['sync'] 34 | 35 | 36 | def sync(conf): 37 | """this should probably be seperated from the class definitions""" 38 | 39 | syncer = carddav.PyCardDAV(conf.account.resource, 40 | user=conf.account.user, 41 | passwd=conf.account.passwd, 42 | write_support=conf.account.write_support, 43 | verify=conf.account.verify, 44 | auth=conf.account.auth) 45 | my_dbtool = backend.SQLiteDb(db_path=conf.sqlite.path, 46 | encoding="utf-8", 47 | errors="stricts", 48 | debug=conf.debug) 49 | # sync: 50 | abook = syncer.get_abook() # type(abook): dict 51 | my_dbtool.check_account_table(conf.account.name, conf.account.resource) 52 | 53 | for href, etag in abook.iteritems(): 54 | if my_dbtool.needs_update(href, conf.account.name, etag=etag): 55 | logging.debug("getting %s etag: %s", href, etag) 56 | vcard = syncer.get_vcard(href) 57 | my_dbtool.update(vcard, conf.account.name, href=href, etag=etag) 58 | 59 | remote_changed = False 60 | # for now local changes overwritten by remote changes 61 | logging.debug("looking for locally changed vcards...") 62 | 63 | hrefs = my_dbtool.get_changed(conf.account.name) 64 | 65 | for href in hrefs: 66 | try: 67 | logging.debug("trying to update %s", href) 68 | card = my_dbtool.get_vcard_from_db(href, conf.account.name) 69 | logging.debug("%s", my_dbtool.get_etag(href, conf.account.name)) 70 | syncer.update_vcard(card.vcf, href, None) 71 | my_dbtool.reset_flag(href, conf.account.name) 72 | remote_changed = True 73 | except carddav.NoWriteSupport: 74 | logging.info('failed to upload changed card {0}, ' 75 | 'you need to enable write support, ' 76 | 'see the documentation', href) 77 | # uploading 78 | hrefs = my_dbtool.get_new(conf.account.name) 79 | for href in hrefs: 80 | try: 81 | logging.debug("trying to upload new card %s", href) 82 | card = my_dbtool.get_vcard_from_db(href, conf.account.name) 83 | (href_new, etag_new) = syncer.upload_new_card(card.vcf) 84 | my_dbtool.update_href(href, 85 | href_new, 86 | conf.account.name, 87 | status=backend.OK) 88 | remote_changed = True 89 | except carddav.NoWriteSupport: 90 | logging.info('failed to upload card %s, ' 91 | 'you need to enable write support, ' 92 | 'see the documentation', href) 93 | 94 | # deleting locally deleted cards on the server 95 | hrefs_etags = my_dbtool.get_marked_delete(conf.account.name) 96 | 97 | for href, etag in hrefs_etags: 98 | try: 99 | logging.debug('trying to delete card %s', href) 100 | syncer.delete_vcard(href, etag) 101 | my_dbtool.delete_vcard_from_db(href, conf.account.name) 102 | remote_changed = True 103 | except carddav.NoWriteSupport: 104 | logging.info('failed to delete card {0}, ' 105 | 'you need to enable write support, ' 106 | 'see the documentation'.format(href)) 107 | 108 | # detecting remote-deleted cards 109 | # is there a better way to compare a list of unicode() with a list of str() 110 | # objects? 111 | 112 | if remote_changed: 113 | abook = syncer.get_abook() # type (abook): dict 114 | r_href_account_list = my_dbtool.get_all_href_from_db_not_new( 115 | [conf.account.name]) 116 | delete = set([href for href, account in r_href_account_list]).difference(abook.keys()) 117 | for href in delete: 118 | my_dbtool.delete_vcard_from_db(href, conf.account.name) 119 | -------------------------------------------------------------------------------- /pycarddav/model.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # vim: set ts=4 sw=4 expandtab sts=4: 3 | # Copyright (c) 2011-2014 Christian Geier & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | """ 24 | The pycarddav abstract model and tools for VCard handling. 25 | """ 26 | 27 | from __future__ import print_function 28 | 29 | import base64 30 | import logging 31 | import sys 32 | from collections import defaultdict 33 | 34 | import vobject 35 | 36 | 37 | def list_clean(string): 38 | """ transforms a comma seperated string to a list, stripping whitespaces 39 | "HOME, WORK,pref" -> ['HOME', 'WORK', 'pref'] 40 | 41 | string: string of comma seperated elements 42 | returns: list() 43 | """ 44 | 45 | string = string.split(',') 46 | rstring = list() 47 | for element in string: 48 | rstring.append(element.strip(' ')) 49 | return rstring 50 | 51 | 52 | NO_STRINGS = [u"n", "n", u"no", "no"] 53 | YES_STRINGS = [u"y", "y", u"yes", "yes"] 54 | 55 | PROPERTIES = ['EMAIL', 'TEL'] 56 | PROPS_ALL = ['FN', 'N', 'VERSION', 'NICKNAME', 'PHOTO', 'BDAY', 'ADR', 57 | 'LABEL', 'TEL', 'EMAIL', 'MAILER', 'TZ', 'GEO', 'TITLE', 'ROLE', 58 | 'LOGO', 'AGENT', 'ORG', 'NOTE', 'REV', 'SOUND', 'URL', 'UID', 59 | 'KEY', 'CATEGORIES', 'PRODID', 'REV', 'SORT-STRING', 'SOUND', 60 | 'URL', 'VERSION', 'UTC-OFFSET'] 61 | PROPS_ALLOWED = ['NICKNAME', 'BDAY', 'ADR', 'LABEL', 'TEL', 'EMAIL', 62 | 'MAILER', 'TZ', 'GEO', 'TITLE', 'ROLE', 'AGENT', 63 | 'ORG', 'NOTE', 'REV', 'SOUND', 'URL', 'UID', 'KEY', 64 | 'CATEGORIES', 'PRODID', 'REV', 'SORT-STRING', 'SOUND', 65 | 'URL', 'VERSION', 'UTC-OFFSET'] 66 | PROPS_ONCE = ['FN', 'N', 'VERSION'] 67 | PROPS_LIST = ['NICKNAME', 'CATEGORIES'] 68 | PROPS_BIN = ['PHOTO', 'LOGO', 'SOUND', 'KEY'] 69 | 70 | 71 | RTEXT = '\x1b[7m' 72 | NTEXT = '\x1b[0m' 73 | BTEXT = '\x1b[1m' 74 | 75 | 76 | def get_names(display_name): 77 | first_name, last_name = '', display_name 78 | 79 | if display_name.find(',') > 0: 80 | # Parsing something like 'Doe, John Abraham' 81 | last_name, first_name = display_name.split(',') 82 | 83 | elif display_name.find(' '): 84 | # Parsing something like 'John Abraham Doe' 85 | # TODO: This fails for compound names. What is the most common case? 86 | name_list = display_name.split(' ') 87 | last_name = ''.join(name_list[-1]) 88 | first_name = ' '.join(name_list[:-1]) 89 | 90 | return first_name.strip().capitalize(), last_name.strip().capitalize() 91 | 92 | 93 | def fix_vobject(vcard): 94 | """trying to fix some more or less common errors in vcards 95 | 96 | for now only missing FN properties are handled (and reconstructed from N) 97 | :type vcard: vobject.base.Component (vobject based vcard) 98 | 99 | """ 100 | if 'fn' not in vcard.contents: 101 | logging.debug('vcard has no formatted name, reconstructing...') 102 | fname = vcard.contents['n'][0].valueRepr() 103 | fname = fname.strip() 104 | vcard.add('fn') 105 | vcard.fn.value = fname 106 | return vcard 107 | 108 | 109 | def vcard_from_vobject(vcard): 110 | vcard = fix_vobject(vcard) 111 | vdict = VCard() 112 | if vcard.name != "VCARD": 113 | raise Exception # TODO proper Exception type 114 | for line in vcard.getChildren(): 115 | # this might break, was tried/excepted before 116 | line.transformFromNative() 117 | property_name = line.name 118 | property_value = line.value 119 | 120 | try: 121 | if line.ENCODING_paramlist == [u'b'] or line.ENCODING_paramlist == [u'B']: 122 | property_value = base64.b64encode(line.value) 123 | 124 | except AttributeError: 125 | pass 126 | if type(property_value) == list: 127 | property_value = (',').join(property_value) 128 | 129 | vdict[property_name].append((property_value, line.params,)) 130 | return vdict 131 | 132 | 133 | def vcard_from_string(vcard_string): 134 | """ 135 | vcard_string: str() or unicode() 136 | returns VCard() 137 | """ 138 | try: 139 | vcard = vobject.readOne(vcard_string) 140 | except vobject.base.ParseError as error: 141 | raise Exception(error) # TODO proper exception 142 | return vcard_from_vobject(vcard) 143 | 144 | 145 | def vcard_from_email(display_name, email): 146 | fname, lname = get_names(display_name) 147 | vcard = vobject.vCard() 148 | vcard.add('n') 149 | vcard.n.value = vobject.vcard.Name(family=lname, given=fname) 150 | vcard.add('fn') 151 | vcard.fn.value = display_name 152 | vcard.add('email') 153 | vcard.email.value = email 154 | vcard.email.type_param = 'INTERNET' 155 | return vcard_from_vobject(vcard) 156 | 157 | 158 | def cards_from_file(cards_f): 159 | collector = list() 160 | for vcard in vobject.readComponents(cards_f): 161 | collector.append(vcard_from_vobject(vcard)) 162 | return collector 163 | 164 | 165 | class VCard(defaultdict): 166 | """ 167 | internal representation of a VCard. This is dict with some 168 | associated methods, 169 | each dict item is a list of tuples 170 | i.e.: 171 | >>> vcard['EMAIL'] 172 | [('hanz@wurst.com', ['WORK', 'PREF']), ('hanz@wurst.net', ['HOME'])] 173 | 174 | self.href: unique id (really just the url) of the VCard 175 | self.account: account which this card is associated with 176 | db_path: database file from which to initialize the VCard 177 | 178 | self.edited: 179 | 0: nothing changed 180 | 1: name and/or fname changed 181 | 2: some property was deleted 182 | """ 183 | 184 | def __init__(self, ddict=''): 185 | 186 | if ddict == '': 187 | defaultdict.__init__(self, list) 188 | else: 189 | defaultdict.__init__(self, list, ddict) 190 | self.href = '' 191 | self.account = '' 192 | self.etag = '' 193 | self.edited = 0 194 | 195 | def serialize(self): 196 | return self.items().__repr__() 197 | 198 | @property 199 | def name(self): 200 | return unicode(self['N'][0][0]) if self['N'] else '' 201 | 202 | @name.setter 203 | def name(self, value): 204 | if not self['N']: 205 | self['N'] = [('', {})] 206 | self['N'][0][0] = value 207 | 208 | @property 209 | def fname(self): 210 | return unicode(self['FN'][0][0]) if self['FN'] else '' 211 | 212 | @fname.setter 213 | def fname(self, value): 214 | self['FN'][0] = (value, {}) 215 | 216 | def alt_keys(self): 217 | keylist = self.keys() 218 | for one in [x for x in ['FN', 'N', 'VERSION'] if x in keylist]: 219 | keylist.remove(one) 220 | keylist.sort() 221 | return keylist 222 | 223 | def print_email(self): 224 | """prints only name, email and type for use with mutt""" 225 | collector = list() 226 | try: 227 | for one in self['EMAIL']: 228 | try: 229 | typelist = ','.join(one[1][u'TYPE']) 230 | except KeyError: 231 | typelist = '' 232 | collector.append(one[0] + "\t" + self.fname + "\t" + typelist) 233 | return '\n'.join(collector) 234 | except KeyError: 235 | return '' 236 | 237 | def print_tel(self): 238 | """prints only name, email and type for use with mutt""" 239 | collector = list() 240 | try: 241 | for one in self['TEL']: 242 | try: 243 | typelist = ','.join(one[1][u'TYPE']) 244 | except KeyError: 245 | typelist = '' 246 | collector.append(self.fname + "\t" + one[0] + "\t" + typelist) 247 | return '\n'.join(collector) 248 | except KeyError: 249 | return '' 250 | 251 | @property 252 | def pretty(self): 253 | return self._pretty_base(self.alt_keys()) 254 | 255 | @property 256 | def pretty_min(self): 257 | return self._pretty_base(['TEL', 'EMAIL']) 258 | 259 | def _pretty_base(self, keylist): 260 | collector = list() 261 | if sys.stdout.isatty(): 262 | collector.append('\n' + BTEXT + 'Name: ' + self.fname + NTEXT) 263 | else: 264 | collector.append('\n' + 'Name: ' + self.fname) 265 | for key in keylist: 266 | for value in self[key]: 267 | try: 268 | types = ' (' + ', '.join(value[1]['TYPE']) + ')' 269 | except KeyError: 270 | types = '' 271 | line = key + types + ': ' + value[0] 272 | collector.append(line) 273 | return '\n'.join(collector) 274 | 275 | def _line_helper(self, line): 276 | collector = list() 277 | for key in line[1].keys(): 278 | collector.append(key + '=' + ','.join(line[1][key])) 279 | if collector == list(): 280 | return '' 281 | else: 282 | return (';' + ';'.join(collector)) 283 | 284 | @property 285 | def vcf(self): 286 | """serialize to VCARD as specified in RFC2426, 287 | if no UID is specified yet, one will be added (as a UID is mandatory 288 | for carddav as specified in RFC6352 289 | TODO make shure this random uid is unique""" 290 | import string 291 | import random 292 | 293 | def generate_random_uid(): 294 | """generate a random uid, when random isn't broken, getting a 295 | random UID from a pool of roughly 10^56 should be good enough""" 296 | choice = string.ascii_uppercase + string.digits 297 | return ''.join([random.choice(choice) for _ in range(36)]) 298 | 299 | if 'UID' not in self.keys(): 300 | self['UID'] = [(generate_random_uid(), dict())] 301 | collector = list() 302 | collector.append('BEGIN:VCARD') 303 | collector.append('VERSION:3.0') 304 | for key in ['FN', 'N']: 305 | try: 306 | collector.append(key + ':' + self[key][0][0]) 307 | except IndexError: # broken vcard without FN or N 308 | collector.append(key + ':') 309 | for prop in self.alt_keys(): 310 | for line in self[prop]: 311 | types = self._line_helper(line) 312 | collector.append(prop + types + ':' + line[0]) 313 | collector.append('END:VCARD') 314 | return '\n'.join(collector) 315 | -------------------------------------------------------------------------------- /pycarddav/ui.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # vim: set ts=4 sw=4 expandtab sts=4 fileencoding=utf-8: 3 | # Copyright (c) 2011-2014 Christian Geier & contributors 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | """ 24 | The pycarddav interface to add, edit, or select a VCard. 25 | """ 26 | 27 | from __future__ import print_function 28 | 29 | try: 30 | import sys 31 | import urwid 32 | 33 | import pycarddav 34 | 35 | except ImportError, error: 36 | print(error) 37 | sys.exit(1) 38 | 39 | 40 | class VCardWalker(urwid.ListWalker): 41 | """A walker to browse a VCard list. 42 | 43 | This walker returns a selectable Text for each of the passed VCard 44 | references. Either accounts or href_account_list needs to be supplied. If 45 | no list of tuples of references are passed to the constructor, then all 46 | cards from the specified accounts are browsed. 47 | """ 48 | 49 | class Entry(urwid.Text): 50 | """A specialized Text which can be used for browsing in a list.""" 51 | _selectable = True 52 | 53 | def keypress(self, _, key): 54 | return key 55 | 56 | class NoEntry(urwid.Text): 57 | """used as an indicator that no match was found""" 58 | _selectable = False 59 | 60 | def __init__(self): 61 | urwid.Text.__init__(self, 'No matching entries found.') 62 | 63 | def __init__(self, database, accounts=None, href_account_list=None, 64 | searchtext=''): 65 | urwid.ListWalker.__init__(self) 66 | self._db = database 67 | self.update(accounts, href_account_list, searchtext) 68 | self._current = 0 69 | 70 | def update(self, accounts=None, href_account_list=None, searchtext=''): 71 | if (accounts is None and href_account_list is None and 72 | searchtext is None): 73 | raise Exception 74 | self._href_account_list = (href_account_list or 75 | self._db.search(searchtext, accounts)) 76 | 77 | @property 78 | def selected_vcard(self): 79 | """Return the focused VCard.""" 80 | return self._db.get_vcard_from_db( 81 | self._href_account_list[self._current].href, 82 | self._href_account_list[self._current].account) 83 | 84 | def get_focus(self): 85 | """Return (focused widget, focused position).""" 86 | return self._get_at(self._current) 87 | 88 | def set_focus(self, pos): 89 | """Focus on pos.""" 90 | self._current = pos 91 | self._modified() 92 | 93 | def get_next(self, pos): 94 | """Return (widget after pos, position after pos).""" 95 | if pos >= len(self._href_account_list) - 1: 96 | return None, None 97 | return self._get_at(pos + 1) 98 | 99 | def get_prev(self, pos): 100 | """Return (widget before pos, position before pos).""" 101 | if pos <= 0: 102 | return None, None 103 | return self._get_at(pos - 1) 104 | 105 | def _get_at(self, pos): 106 | """Return a textual representation of the VCard at pos.""" 107 | if pos >= len(self._href_account_list): 108 | return VCardWalker.NoEntry(), pos 109 | vcard = self._db.get_vcard_from_db(self._href_account_list[pos].href, 110 | self._href_account_list[pos].account 111 | ) 112 | label = vcard.fname 113 | if vcard['EMAIL']: 114 | label += ' (%s)' % vcard['EMAIL'][0][0] 115 | return urwid.AttrMap(VCardWalker.Entry(label), 'list', 'list focused'), pos 116 | 117 | 118 | class SearchField(urwid.WidgetWrap): 119 | """a search widget""" 120 | _selectable = True 121 | 122 | def __init__(self, updatefunc, window): 123 | self.updatefunc = updatefunc 124 | self.window = window 125 | self.edit = urwid.AttrWrap(urwid.Edit(caption=('', 'Search for: ')), 126 | 'edit', 'edit focused') 127 | self.cancel = urwid.AttrWrap( 128 | urwid.Button(label='Cancel', on_press=self.destroy), 129 | 'button', 'button focused') 130 | self.search = urwid.AttrWrap( 131 | urwid.Button(label='Search', on_press=self.search, 132 | user_data=self.edit), 'button', 'button focused') 133 | buttons = urwid.GridFlow([self.cancel, self.search], 10, 3, 1, 'left') 134 | widget = urwid.Pile([self.edit, 135 | urwid.Padding(buttons, 'right', 26, 1, 1, 1)]) 136 | urwid.WidgetWrap.__init__(self, urwid.Padding(widget, 'center', left=1, 137 | right=1)) 138 | 139 | def search(self, button, text_edit): 140 | search_text = text_edit.get_edit_text() 141 | self.updatefunc(search_text) 142 | self.window.backtrack() 143 | 144 | def destroy(self, button): 145 | self.window.backtrack() 146 | 147 | 148 | class Pane(urwid.WidgetWrap): 149 | """An abstract Pane to be used in a Window object.""" 150 | def __init__(self, widget, title=None, description=None): 151 | self.widget = widget 152 | urwid.WidgetWrap.__init__(self, widget) 153 | self._title = title or '' 154 | self._description = description or '' 155 | self.window = None 156 | 157 | @property 158 | def title(self): 159 | return self._title 160 | 161 | @property 162 | def description(self): 163 | return self._description 164 | 165 | def get_keys(self): 166 | """Return a description of the keystrokes recognized by this pane. 167 | 168 | This method returns a list of tuples describing the keys 169 | handled by a pane. This list is used to build a contextual 170 | pane help. Each tuple is a pair of a list of keys and a 171 | description. 172 | 173 | The abstract pane returns the default keys handled by the 174 | window. Panes which do not override there keys should extend 175 | this list. 176 | """ 177 | return [(['up', 'down', 'pg.up', 'pg.down'], 178 | 'navigate through the fields.'), 179 | (['esc'], 'backtrack to the previous pane or exit.'), 180 | (['F1', '?'], 'open this pane help.')] 181 | 182 | 183 | class HelpPane(Pane): 184 | """A contextual help screen.""" 185 | def __init__(self, pane): 186 | content = [] 187 | for key_list, description in pane.get_keys(): 188 | key_text = [] 189 | for key in key_list: 190 | if key_text: 191 | key_text.append(', ') 192 | key_text.append(('bright', key)) 193 | content.append( 194 | urwid.Columns( 195 | [urwid.Padding(urwid.Text(key_text), left=10), 196 | urwid.Padding(urwid.Text(description), right=10)])) 197 | 198 | Pane.__init__(self, urwid.ListBox(urwid.SimpleListWalker(content)), 199 | 'Help') 200 | 201 | 202 | class VCardChooserPane(Pane): 203 | """A VCards chooser. 204 | 205 | This pane allows to browse a list of VCards. If no references are 206 | passed to the constructor, then the whole database is browsed. A 207 | VCard can be selected to be used in another pane, like the 208 | EditorPane. 209 | """ 210 | def __init__(self, database, accounts=None, href_account_list=None): 211 | self.database = database 212 | self.accounts = accounts 213 | self._walker = VCardWalker(database, 214 | accounts=accounts, 215 | href_account_list=href_account_list) 216 | Pane.__init__(self, urwid.ListBox(self._walker), 'Browse...') 217 | 218 | def get_keys(self): 219 | keys = Pane.get_keys(self) 220 | keys.append(([' ', 'enter'], 'select a contact.')) 221 | keys.append((['/'], 'search for contacts')) 222 | return keys 223 | 224 | def keypress(self, size, key): 225 | self._w.keypress(size, key) 226 | if key in ['space', 'enter']: 227 | self.window.backtrack(self._walker.selected_vcard) 228 | if key in ['/']: 229 | self.search() 230 | else: 231 | return key 232 | 233 | def search(self): 234 | search = urwid.LineBox(SearchField(self.update, self.window)) 235 | self.window.overlay(search, 'Search') 236 | 237 | def update(self, searchtext): 238 | self._walker = VCardWalker(self.database, accounts=self.accounts, 239 | searchtext=searchtext) 240 | self._w = urwid.ListBox(self._walker) 241 | 242 | 243 | class EditorPane(Pane): 244 | """A VCard editor.""" 245 | def __init__(self, database, account, vcard): 246 | self._vcard = vcard 247 | self._db = database 248 | self._account = account 249 | 250 | self._label = vcard.fname if vcard.fname else vcard['EMAIL'][0][0] 251 | self._fname_edit = urwid.Edit(u'', u'') 252 | self._lname_edit = urwid.Edit(u'', u'') 253 | self._email_edits = None 254 | 255 | Pane.__init__(self, self._build_ui(), 'Edit %s' % vcard.fname) 256 | 257 | def get_keys(self): 258 | keys = Pane.get_keys(self) 259 | keys.append((['F8'], 'save this contact.')) 260 | return keys 261 | 262 | def keypress(self, size, key): 263 | self._w.keypress(size, key) 264 | if key == 'f8': 265 | self._validate() 266 | self.window.backtrack() 267 | else: 268 | return key 269 | 270 | def on_button_press(self, button): 271 | if button.get_label() == 'Merge': 272 | self.window.open(VCardChooserPane(self._db, 273 | accounts=[self._account]), 274 | self.on_merge_vcard) 275 | else: 276 | if button.get_label() == 'Store': 277 | self._validate() 278 | self.window.backtrack() 279 | 280 | def on_merge_vcard(self, vcard): 281 | # TODO: this currently merges only one email field, which is ok to use with mutt. 282 | if vcard: 283 | vcard['EMAIL'].append(self._vcard['EMAIL'][0]) 284 | self._vcard = vcard 285 | self._w = self._build_ui() 286 | self._status = pycarddav.backend.CHANGED 287 | 288 | def _build_ui(self): 289 | content = [] 290 | content.extend(self._build_names_section()) 291 | content.extend(self._build_emails_section()) 292 | content.extend(self._build_buttons_section()) 293 | 294 | return urwid.ListBox(urwid.SimpleListWalker(content)) 295 | 296 | def _build_names_section(self): 297 | names = self._vcard.name.split(';') 298 | if len(names) > 1: 299 | self._lname_edit.set_edit_text(names[0]) 300 | self._fname_edit.set_edit_text(names[1]) 301 | else: 302 | self._lname_edit.set_edit_text(u'') 303 | self._fname_edit.set_edit_text(names[0]) 304 | 305 | return [urwid.Divider(), 306 | urwid.Columns([ 307 | ('fixed', 15, urwid.AttrWrap(urwid.Text(u'First Name'), 'line header')), 308 | urwid.AttrWrap(self._fname_edit, 'edit', 'edit focused')]), 309 | urwid.Divider(), 310 | urwid.Columns([ 311 | ('fixed', 15, urwid.AttrWrap(urwid.Text(u'Last Name'), 'line header')), 312 | urwid.AttrWrap(self._lname_edit, 'edit', 'edit focused')])] 313 | 314 | def _build_emails_section(self): 315 | self._email_edits = [] 316 | content = [] 317 | for mail in self._vcard['EMAIL']: 318 | edit = urwid.Edit('', mail[0]) 319 | self._email_edits.append(edit) 320 | content.extend([ 321 | urwid.Divider(), 322 | urwid.Columns([ 323 | ('fixed', 15, urwid.AttrWrap(urwid.Text(u'Email'), 'line header')), 324 | urwid.AttrWrap(edit, 'edit', 'edit focused')])]) 325 | 326 | return content 327 | 328 | def _build_buttons_section(self): 329 | buttons = [u'Cancel', u'Merge', u'Store'] 330 | row = urwid.GridFlow([urwid.AttrWrap(urwid.Button(lbl, self.on_button_press), 331 | 'button', 'button focused') for lbl in buttons], 332 | 10, 3, 1, 'left') 333 | return [urwid.Divider('-', 1, 1), 334 | urwid.Padding(row, 'right', 13 * len(buttons), None, 1, 1)] 335 | 336 | def _validate(self): 337 | self._vcard.fname = ' '.join( 338 | [self._fname_edit.edit_text, self._lname_edit.edit_text]) 339 | for i, edit in enumerate(self._email_edits): 340 | self._vcard['EMAIL'][i] = (edit.edit_text, self._vcard['EMAIL'][i][1]) 341 | if(hasattr(self, '_status')): 342 | status = self._status 343 | else: 344 | status = pycarddav.backend.NEW 345 | self._db.update(self._vcard, 346 | self._account, 347 | self._vcard.href, 348 | etag=self._vcard.etag, 349 | status=status) 350 | 351 | 352 | class Window(urwid.Frame): 353 | """The main user interface frame. 354 | 355 | A window is a frame which displays a header, a footer and a body. 356 | The header and the footer are handled by this object, and the body 357 | is the space where Panes can be displayed. 358 | 359 | Each Pane is an interface to interact with the database in one 360 | way: list the VCards, edit one VCard, and so on. The Window 361 | provides a mechanism allowing the panes to chain themselves, and 362 | to carry data between them. 363 | """ 364 | PALETTE = [('header', 'white', 'black'), 365 | ('footer', 'white', 'black'), 366 | ('line header', 'black', 'white', 'bold'), 367 | ('bright', 'dark blue', 'white', ('bold', 'standout')), 368 | ('list', 'black', 'white'), 369 | ('list focused', 'white', 'light blue', 'bold'), 370 | ('edit', 'black', 'white'), 371 | ('edit focused', 'white', 'light blue', 'bold'), 372 | ('button', 'black', 'dark cyan'), 373 | ('button focused', 'white', 'light blue', 'bold')] 374 | 375 | def __init__(self): 376 | self._track = [] 377 | self._title = u' {0} v{1}'.format(pycarddav.__productname__, 378 | pycarddav.__version__) 379 | 380 | header = urwid.AttrWrap(urwid.Text(self._title), 'header') 381 | footer = urwid.AttrWrap(urwid.Text( 382 | u' Use Up/Down/PgUp/PgDown:scroll. Esc: return. ?: help'), 383 | 'footer') 384 | urwid.Frame.__init__(self, urwid.Text(''), 385 | header=header, 386 | footer=footer) 387 | self._original_w = None 388 | 389 | def open(self, pane, callback=None): 390 | """Open a new pane. 391 | 392 | The given pane is added to the track and opened. If the given 393 | callback is not None, it will be called when this new pane 394 | will be closed. 395 | """ 396 | pane.window = self 397 | self._track.append((pane, callback)) 398 | self._update(pane) 399 | 400 | def overlay(self, overlay_w, title): 401 | """put overlay_w as an overlay over the currently active pane 402 | """ 403 | overlay = Pane(urwid.Overlay(urwid.Filler(overlay_w), 404 | self._get_current_pane(), 405 | 'center', 60, 406 | 'middle', 5), title) 407 | self.open(overlay) 408 | 409 | def backtrack(self, data=None): 410 | """Unstack the displayed pane. 411 | 412 | The current pane is discarded, and the previous one is 413 | displayed. If the current pane was opened with a callback, 414 | this callback is called with the given data (if any) before 415 | the previous pane gets redrawn. 416 | """ 417 | _, cb = self._track.pop() 418 | if cb: 419 | cb(data) 420 | 421 | if self._track: 422 | self._update(self._get_current_pane()) 423 | else: 424 | raise urwid.ExitMainLoop() 425 | 426 | def on_key_press(self, key): 427 | """Handle application-wide key strokes.""" 428 | if key == 'esc': 429 | self.backtrack() 430 | elif key in ['f1', '?']: 431 | self.open(HelpPane(self._get_current_pane())) 432 | 433 | def _update(self, pane): 434 | self.header.w.set_text(u'%s | %s' % (self._title, pane.title)) 435 | self.set_body(pane) 436 | 437 | def _get_current_pane(self): 438 | return self._track[-1][0] if self._track else None 439 | 440 | 441 | def start_pane(pane): 442 | """Open the user interface with the given initial pane.""" 443 | frame = Window() 444 | frame.open(pane) 445 | loop = urwid.MainLoop(frame, Window.PALETTE, 446 | unhandled_input=frame.on_key_press) 447 | loop.run() 448 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | vobject 3 | pytest 4 | urwid 5 | lxml 6 | pyxdg 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | 3 | import os 4 | import string 5 | import subprocess 6 | import sys 7 | import warnings 8 | 9 | #from distutils.core import setup 10 | from setuptools import setup 11 | 12 | MAJOR = 0 13 | MINOR = 7 14 | PATCH = 0 15 | 16 | RELEASE = False 17 | 18 | VERSION = "{0}.{1}.{2}".format(MAJOR, MINOR, PATCH) 19 | 20 | if not RELEASE: 21 | try: 22 | try: 23 | pipe = subprocess.Popen(["git", "describe", "--dirty", "--tags"], 24 | stdout=subprocess.PIPE) 25 | except EnvironmentError: 26 | warnings.warn("WARNING: git not installed or failed to run") 27 | 28 | revision = pipe.communicate()[0].strip().lstrip('v') 29 | if pipe.returncode != 0: 30 | warnings.warn("WARNING: couldn't get git revision") 31 | 32 | if revision != VERSION: 33 | revision = revision.lstrip(string.digits + '.') 34 | VERSION += '.dev' + revision 35 | except: 36 | VERSION += '.dev' 37 | warnings.warn("WARNING: git not installed or failed to run") 38 | 39 | 40 | def write_version(): 41 | """writes the pycarddav/version.py file""" 42 | template = """\ 43 | __version__ = '{0}' 44 | """ 45 | filename = os.path.join( 46 | os.path.dirname(__file__), 'pycarddav', 'version.py') 47 | with open(filename, 'w') as versionfile: 48 | versionfile.write(template.format(VERSION)) 49 | print("wrote pycarddav/version.py with version={0}".format(VERSION)) 50 | 51 | write_version() 52 | 53 | 54 | requirements = [ 55 | 'lxml', 56 | 'vobject', 57 | 'requests', 58 | 'urwid', 59 | 'pyxdg' 60 | ] 61 | if sys.version_info[:2] in ((2, 6),): 62 | # there is no argparse in python2.6 63 | requirements.append('argparse') 64 | 65 | setup( 66 | name='pyCardDAV', 67 | version=VERSION, 68 | description='A CardDAV based address book tool', 69 | long_description=open('README.rst').read(), 70 | author='Christian Geier', 71 | author_email='pycarddav@lostpackets.de', 72 | url='http://lostpackets.de/pycarddav/', 73 | license='Expat/MIT', 74 | packages=['pycarddav', 'pycarddav.controllers'], 75 | scripts=['bin/pycardsyncer', 'bin/pc_query', 'bin/pycard-import'], 76 | requires=requirements, 77 | install_requires=requirements, 78 | classifiers=[ 79 | "Development Status :: 4 - Beta", 80 | "License :: OSI Approved :: MIT License", 81 | "Environment :: Console :: Curses", 82 | "Intended Audience :: End Users/Desktop", 83 | "Operating System :: POSIX", 84 | "Programming Language :: Python :: 2 :: Only", 85 | "Topic :: Utilities", 86 | "Topic :: Communications :: Email :: Address Book" 87 | ], 88 | ) 89 | -------------------------------------------------------------------------------- /tests/README.rst: -------------------------------------------------------------------------------- 1 | Tests 2 | ===== 3 | 4 | The tests in local can be run everywhere and do not have any outside 5 | dependencies. The tests in vagrant need a specific vagrant (Virtualbox) 6 | machine running for testing against different CardDAV servers. See 7 | vagrant/README.rst 8 | -------------------------------------------------------------------------------- /tests/assets/README.txt: -------------------------------------------------------------------------------- 1 | this dir contains some vcf files for testing 2 | 3 | * hanz.vcf: created by owncloud 4 | * lenna.vcf: created by evolution, contains image 5 | * mueller.vcf: cointains some non ascii characters 6 | * dexter.vcf: contains a NICKNAME with two values 7 | -------------------------------------------------------------------------------- /tests/assets/configs/base.conf: -------------------------------------------------------------------------------- 1 | [Account work] 2 | resource: http://test.com/abook/collection 3 | user: testman 4 | passwd: foobar 5 | verify: False 6 | auth: basic 7 | 8 | [Account work_no_verify] 9 | resource: http://test.com/abook/collection 10 | user: testman 11 | passwd: foobar 12 | auth: basic 13 | 14 | [Account davical] 15 | user: tester 16 | passwd: barfoo23 17 | resource: https://carddavcentral.com:4443/caldav.php/tester/abook/ 18 | verify: /home/testman/.pycard/cacert.pem 19 | auth: digest 20 | write_support = YesPleaseIDoHaveABackupOfMyData 21 | 22 | 23 | [sqlite] 24 | path: /home/testman/.pycard/abook.db 25 | 26 | 27 | [default] 28 | debug: False 29 | -------------------------------------------------------------------------------- /tests/assets/dexter.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | N:Morgan;Dexter 4 | FN:Dexter Morgan 5 | ORG:Bubba Gump Shrimp Co. 6 | NICKNAME:Bay Harbour Butcher,Dex 7 | TEL;TYPE=WORK,VOICE:(42) 111-222 8 | END:VCARD 9 | 10 | -------------------------------------------------------------------------------- /tests/assets/gödel.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | N:Gödel;François 4 | FN:François Gödel 5 | TEL;TYPE=WORK,VOICE:+49-123-678901 6 | TEL;TYPE=HOME,VOICE:(101) 1234 4123 7 | ADR;TYPE=WORK:;;Essalág 100;Torshavn;50800;Færøerne 8 | EMAIL;TYPE=PREF,INTERNET:francois@goedel.net 9 | END:VCARD 10 | -------------------------------------------------------------------------------- /tests/assets/hanz.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | EMAIL;TYPE=PREF:hanz@wurst.net 4 | N:Hanz Wurst;;;; 5 | FN:Hanz Wurst 6 | REV:2012-08-02T21:16:14+00:00 7 | PRODID:-//ownCloud//NONSGML Contacts 0.2//EN 8 | UID:c292c7212b 9 | END:VCARD 10 | -------------------------------------------------------------------------------- /tests/assets/lenna.vcf: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | TEL;X-EVOLUTION-UI-SLOT=1;TYPE=WORK,VOICE:+42-0235-123456 4 | URL: 5 | TITLE: 6 | ROLE: 7 | X-EVOLUTION-MANAGER: 8 | X-EVOLUTION-ASSISTANT: 9 | NICKNAME:Lena 10 | X-EVOLUTION-SPOUSE: 11 | NOTE: 12 | FN:Lenna Test 13 | N:Test;Lenna;;; 14 | X-EVOLUTION-BLOG-URL: 15 | CALURI: 16 | FBURL: 17 | X-EVOLUTION-VIDEO-URL: 18 | X-MOZILLA-HTML:TRUE 19 | PHOTO;ENCODING=b;TYPE="X-EVOLUTION-UNKNOWN":/9j/4AAQSkZJRgABAQAAAQABAAD/2wB 20 | DAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0H 21 | yc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjI 22 | yMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCABgAGADASIAAhEBAxEB/8QAHwAAAQUBAQEBA 23 | QEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1F 24 | hByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV 25 | 1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usL 26 | DxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAA 27 | AAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoE 28 | IFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZ 29 | GVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcb 30 | HyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDp0QswZug5rmPFk 31 | he+tDnqjf0rUOrpN4hTTIeUjBMjA9/Ssbxg2zUbMD+439K+fpt86PpWtCjYn5ic1qK2MVj2T7W 32 | PFaPnKiBiCcnAUdTVvcqKViyz4Gaasilsc5xUlrJGzkugb2bmurstPsb2BTJaQ7fZdp/Mc1tGi 33 | 5dTCriY0+hyDHn2pLOUw6rGwOA3DV0974MuGVpdKcSL1EUpwR7BuhrjLaYvqscTIY3VyrIRgqR 34 | 1B9wRQ6UobhCtTqr3XqdgVTAx781d0e+W0uzDcYNvcDy5Ae3v9O341hTXqW08VvKwDy52Z7n0o 35 | cs5HPP+RXM0ky3T54uJS1vR5NE1iWyIPlH54GP8SHpn6cj8K4fxF4oaBzZ6VKRIhxJcqcbT6Lz 36 | x7n6V0fxM8YrNpNjpEbN/akJPnTqfuREdM9mOB+APrXmFpYXOozJa2kRd2baAPU8fh1/CvQoUr 37 | rnmedUryS9n1PSfC7M2vozklmDZY9zV3xoduo2eR/A39Kr+GcNrcWBjg1d8bqBqVlj+43X8K47 38 | r2qPS+yZNirPKI0Us7sFUAdzwP51Zlyb+aNSCI3KjHQ4JH/1/xpfDIEviCyjbp5vmceqgsP8A0 39 | GnzWsltqrx8szEt+ZpprmGr7GjZxn5IrdC8ren88+ldto9strCvmESydT/cU+v/ANeuT07bb4Y 40 | HcT95v73sPYf59K6SO7yMFsADp6V10tNThxKb0R1Ec3O4uS3rXF+NtEgi1Kx163h2TPMIrwr0Y 41 | Fflc9geME9ywrYhv9y43EZHp2qxHc/akMKKrwkYbceMfy966pWqRaOCHNSmpI8k+IX2kLpgs43 42 | kujcgRRou5mbHAx3rq7zTtX0zwXLr13bRGaC186S2D5IPHU9CO5x2FdnY6PZWsiSpArzgFRK4B 43 | YZHQHHT6Y7VzXxH8S21t4C1iJTma52WsZIyG3nLc+6q/wCWe9Y08PHkSmdFXGTc26ex89ySz3t 44 | 008sjy3Mz73bGWLE+n1P8q9b8H+Gl0exR7hB9pcZk/wBn/ZB9B39TmuM+H2kLqGsveSpujtAGX 45 | PQOenHfHP6V6yVAG1TwOBRVlZcqFSg378tzkfDQVNbjQ9SOKteO8i+sPUq/9Ko6ASfENv2yDWj 46 | 46X/TtO4z8rf0rzrfvEeq3oY+h3i6brdpfspMcEhLgcnaQVPHcgEmun8RwGDWAS4KuDIm3urHo 47 | Py/KuWsYxM5RAGZ+nP61NrOuL5BuMl7lEFtHGVYZMYClsEZHAz6HkVrCm5VFYzqS5HzNkuoa5H 48 | pUfmPlmCk4BHCiuaXxZcC9k3hY94VQrEMVJwd3PAIPoPx7nnri4uLm/3TTbp2kI3E9sce2OMj1 49 | 5PvXfeB/CsepvDrWoW48mMBIUf7pBJJc55PfGeABx6L60FCjHU8urVlVl7pq+GtE1rVJY9S1LV 50 | pksYmBFsGy0rA5BLdDkY7d/YrXoU16mn6ZNcrbSSR26byseCWx6Z/z6Vgf2zYRzxWQnWNFcqjl 51 | SFbnoD0HBHWun+16fBpbm7mjigZCpZmwDx09zWMppyuh+xbjqcXqfjp7jR5bxb6PS7CMbXKL5s 52 | shIPyqeME9uOPXFeR+I/ELa59ntLeJ4rKF2eNXbdLLI3WSRu7HAAHQfiSZrld2i6tZKWMdrqUc 53 | se4YO0hx9fSo/C2nDUvEsAIzHB+8POc7Txn/gWD+Bod0+ZktfZR6Z4O0YaToMasQZXJaQ46se3 54 | 4cD8K2mUjmhV8lNsb4CgcHvUUlyyj51wD3HNcrfM2zoTsrHDeHbhT4ss4wxPDd66XxxCJL/T1A 55 | cna4+Xk9q4zwhE0vi21kRwFRWJB69K7Xxhg6hpmVVtyuNrfUHOaxcL1UkdsZaXZzTao2gLcW0S 56 | JdX12QIwrBRBgkc5JYnBHHA+76Vz1u6y69cfabzeyODNK7BS5xnnOMAHt0XbnrU8d4smoW7Sja 57 | S28KTjB3A9vwxn+QBOcNGZdZEZiFwlwwSKOIldwbDAr7c7Tk5554Oa9yNKFCF4rU8apUnUqNX0 58 | I/D+gXGueI4dGdSFVsSlhhtg6fz/LNetXstv4g01tE02cxWCDZ50TY80jg4/2Aen97r9fP9Vvr 59 | vw9Bqkpkt01jUw1u62/S3hU/Nhs5LHlMnsrYORXUeGR9ktI4gQVjJAI9q8/EVNmd2CoqcmmW4f 60 | B9ppemLtttl0kzSLceYSxVgFKY/ujAPOeenrV/wASaeNT8LaXK6edGnnZgZtqu5GATwc47Vbvr 61 | ia6tlVGXfnpnk+1VfEF4+geDUN9GFZJJGRQ+c9AB+dYxk5SOupCMY2fc8p0yHyLDXonbeYYg/8 62 | AvFZFGf8Ax79K6b4aWHl2dxfsuTIwUZHZf/rk/lXM6Fummu4JOXv7C5X/AIEQHH/oH616doNkN 63 | L0C1tTguEG8+rYyf1JrWpO0bHnqK5m1sX5JQi5NXbTQp7yNZbhzHG3IjXrj+lQaXarf6pHE3KK 64 | C7D1xXexQBRzgnua5XJ35YhUdj51+HTF9cEhwcBV6+ua7Xx3dJZtaTHaNkchwe5BAAPtnH5Y9a 65 | 5fR9HTRvEOk3mnzl7C/fA9Puk1r/FedrbR7YKxEkreUCDjOQSe3TAxjjqPQVs42xCRtCp+4cji 66 | dHSCS+u45nK7EwrNzgb0Tn9fwwK66wvrfw7Z3d5dBkn+zuqMoyWO4DYh5A5bdu9cHkgiuIs7yC 67 | 1m89SzCYtFKWUAFCBlRjPQ4IPXnua6nT5f7Q0+eFoxtVSBbucqRjGff+9n1A56Y9xJVKfKeSpO 68 | MrnC3N097eyXEwAMy4CgYVRggAewA7nPHPPJ6/wAOa8g228jhSWyu7v0rkdQtRa3zW28bVVNjZ 69 | yGVl3Ag9xg9a3fDNqGvYmmRJFliLoGGeQcH+YryMRBWsz0sFUkp6HpEVlJdTebC7bD95fOZVP5 70 | GuK+I96TNbaYGYMv76T94zgcbQBuJ9CeK7iwQxbVQKnqVGMVq6r4H0XxdZ7HT7LfBcJdwqA3sG 71 | H8Qz+NYUOVM68bOXJax5B4cg/4n2knBwTjHoGRgf516qdpjHPA6Yrz+z0y80PxvDo1+irPbAjK 72 | fddBGxV19j/nFdRPaywAm3uHQjsTkGrmryOKL9063wgA+s3Bxwluf5iuzmljtkZ3dI0Xks5wMe 73 | uT0rgvApnhbUr27cCFIlJk246Fif0ArzvxRrF94wvmmuWaOwjYiG1DHaRnhm9ScfhWO83Ynkcm 74 | P8NyNceFvCU3J+zag0Z9PuuB/OrvxeydO045AHmn9RU3hbGoeCYp4YY0S11CPcB0DfL/8VVf4v 75 | vm30eIdS8jHH4D+tbyu8VEIaYeS/roeZPKfscUajHlMzKe5LY5Pv8mM/pXTaFemzs/NkjaaN1e 76 | KJ9u4erK3tjBGevPrxhWlkJG8qUsqPhS3YfI5z+G0fnWjpzvZRXNvvKvLEPk6hnH9QQ6161Nta 77 | nnyK2pTR6rrHkWwy2UhVkfehRVCAgkA4AAHOfrXaaTYxJqEMUOALKDy3/32wSPyAP4iuR8K6Ze 78 | PcvcrtiIG0M65KnuQK9E0y0jsbfyUy2TuZm6sx6k+5NeLiqqcmke7gKDUVJo0UJjk68Gur0K6A 79 | k5PauWC+Yg9a0tKdophziuelKzOrFQUok3xC062hvtN8TyDBtontJNoyW3YZfyxJ/33XLJqNpq 80 | B8u3uozI3RM4b8vWvRtegGr+DtRtgMyeSZIwO7p86/qoFeLWlhDLqVnqmzL2sqShV45DA4/Su2 81 | rb4jyKSduU9E8TatFYeAre3iQwfaSLcg8Fwv3j9Ca8ykuAsHmYwqLxnnA9av+KdXvdW1SOS7TZ 82 | BAG+zWsfRdxzknuTxz7Vg6qTb28dkcSXc+HkRe3tWNCm7czNpS5E42P/Z 83 | X-EVOLUTION-FILE-AS:Test\, Lenna 84 | EMAIL;X-EVOLUTION-UI-SLOT=1;TYPE=WORK:lenna@test.org 85 | UID:pas-id-501AEF6D00000000 86 | REV:2012-08-02T21:21:49Z 87 | END:VCARD -------------------------------------------------------------------------------- /tests/local/output/serialize_to_vcf.out: -------------------------------------------------------------------------------- 1 | BEGIN:VCARD 2 | VERSION:3.0 3 | FN:François Gödel 4 | N:Gödel;François;;; 5 | ADR;TYPE=WORK:;;Essalág 100;Torshavn;50800;Færøerne; 6 | EMAIL;TYPE=PREF,INTERNET:francois@goedel.net 7 | TEL;TYPE=WORK,VOICE:+49-123-678901 8 | TEL;TYPE=HOME,VOICE:(101) 1234 4123 9 | UID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA 10 | END:VCARD 11 | -------------------------------------------------------------------------------- /tests/local/output/vcard_insert1.out: -------------------------------------------------------------------------------- 1 | [(u'/something.vcf', u'', u'G\xf6del;Fran\xe7ois;;;', u'Fran\xe7ois G\xf6del', u'BEGIN:VCARD\nVERSION:3.0\nFN:Fran\xe7ois G\xf6del\nN:G\xf6del;Fran\xe7ois;;;\nADR;TYPE=WORK:;;Essal\xe1g 100;Torshavn;50800;F\xe6r\xf8erne;\nEMAIL;TYPE=PREF,INTERNET:francois@goedel.net\nTEL;TYPE=WORK,VOICE:+49-123-678901\nTEL;TYPE=HOME,VOICE:(101) 1234 4123\nUID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA\nEND:VCARD', 0)] 2 | -------------------------------------------------------------------------------- /tests/local/output/vcard_insert_with_status.out: -------------------------------------------------------------------------------- 1 | [(u'/something.vcf', u'', u'G\xf6del;Fran\xe7ois;;;', u'Fran\xe7ois G\xf6del', u'BEGIN:VCARD\nVERSION:3.0\nFN:Fran\xe7ois G\xf6del\nN:G\xf6del;Fran\xe7ois;;;\nADR;TYPE=WORK:;;Essal\xe1g 100;Torshavn;50800;F\xe6r\xf8erne;\nEMAIL;TYPE=PREF,INTERNET:francois@goedel.net\nTEL;TYPE=WORK,VOICE:+49-123-678901\nTEL;TYPE=HOME,VOICE:(101) 1234 4123\nUID:E41JRQX2DB4P1AQZI86BAT7NHPBHPRIIHQKA\nEND:VCARD', 1)] 2 | -------------------------------------------------------------------------------- /tests/local/pycard_test.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | import pycarddav.model 3 | import pycarddav.backend as backend 4 | import os.path 5 | import pytest 6 | import random 7 | 8 | 9 | # some helper functions 10 | 11 | def get_basename(): 12 | curdir = os.path.basename(os.path.abspath(os.path.curdir)) 13 | if os.path.isdir('tests') and curdir == 'pycarddav': 14 | basepath = 'tests/' 15 | elif os.path.isdir('assets') and curdir == 'tests': 16 | basepath = './' 17 | elif os.path.isdir('pycarddav') and curdir == 'pycarddav': 18 | basepath = 'pycarddav/tests/' 19 | elif curdir == 'local': 20 | basepath = '../' 21 | else: 22 | raise Exception("don't know where I'm") 23 | return basepath 24 | 25 | basepath = get_basename() 26 | 27 | 28 | def get_vcard(cardname): 29 | """gets a vcard from the assets directory""" 30 | filename = basepath + 'assets/' + cardname + '.vcf' 31 | with file(filename) as vcard: 32 | cardstring = vcard.read() 33 | return pycarddav.model.vcard_from_string(cardstring) 34 | 35 | 36 | def get_output(function_name): 37 | with file(basepath + 'local/output/' + function_name + '.out') as output_file: 38 | output = output_file.readlines() 39 | return ''.join(output).strip('\n') 40 | 41 | # \helper functions 42 | 43 | 44 | def pytest_funcarg__emptydb(request): 45 | mydb = backend.SQLiteDb(db_path=':memory:') 46 | mydb.check_account_table('test', 'http://test.com') 47 | return mydb 48 | 49 | ## tests 50 | 51 | 52 | def test_serialize_to_vcf(): 53 | random.seed(1) 54 | assert get_vcard('gödel').vcf.encode('utf-8') == get_output('serialize_to_vcf') 55 | 56 | 57 | def test_broken_nobegin(): 58 | with pytest.raises(Exception) as error: 59 | get_vcard('broken_nobegin') 60 | print error 61 | 62 | def test_db_init(emptydb): 63 | assert emptydb._dump('test') == list() 64 | 65 | 66 | def test_vcard_insert1(emptydb): 67 | random.seed(1) 68 | emptydb.check_account_table('test', 'http://test.com') 69 | emptydb.update(get_vcard('gödel').vcf, 'test', href='/something.vcf') 70 | assert str(emptydb._dump('test')) == get_output('vcard_insert1') 71 | 72 | 73 | def test_vcard_insert_with_status(emptydb): 74 | random.seed(1) 75 | emptydb.check_account_table('test', 'http://test.com') 76 | emptydb.update(get_vcard('gödel').vcf, 77 | 'test', 78 | href='/something.vcf', 79 | status=backend.NEW) 80 | assert str(emptydb._dump('test')) == get_output('vcard_insert_with_status') 81 | -------------------------------------------------------------------------------- /tests/local/pycarddav_test.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | """these test should test code defined in pycarddav/__init__.py (mainly 3 | the configuration parsing)""" 4 | import os.path 5 | import sys 6 | 7 | from pycarddav import ConfigurationParser 8 | from pycarddav import SyncConfigurationParser 9 | 10 | # some helper functions 11 | 12 | def get_basename(): 13 | """find the base path so we can build proper paths, needed so we can start 14 | the tests from anywhere""" 15 | curdir = os.path.basename(os.path.abspath(os.path.curdir)) 16 | if os.path.isdir('tests') and curdir == 'pycarddav': 17 | basepath = 'tests/' 18 | elif os.path.isdir('assets') and curdir == 'tests': 19 | basepath = './' 20 | elif os.path.isdir('pycarddav') and curdir == 'pycarddav': 21 | basepath = 'pycarddav/tests/' 22 | elif curdir == 'local': 23 | basepath = '../' 24 | else: 25 | raise Exception("don't know where I'm") 26 | return basepath 27 | 28 | basepath = get_basename() 29 | 30 | 31 | 32 | def test_basic_config(): 33 | """testing the basic configuration parser 34 | this rather complicated setup is needed, since py2.6 and py2.7 return 35 | the accounts list in different orders""" 36 | sys.argv = ['pycardsyncer', '-c', 37 | '{0}/assets/configs/base.conf'.format(basepath)] 38 | conf_parser = ConfigurationParser('let\'s do a test', check_accounts=False) 39 | conf = conf_parser.parse() 40 | 41 | assert conf.debug == False 42 | assert conf.sqlite.path == '/home/testman/.pycard/abook.db' 43 | assert conf.filename.endswith('tests//assets/configs/base.conf') == True 44 | def assert_work(accounts_conf): 45 | assert accounts_conf.write_support == False 46 | assert accounts_conf.resource == 'http://test.com/abook/collection' 47 | assert accounts_conf.name == 'work' 48 | assert accounts_conf.passwd == 'foobar' 49 | assert accounts_conf.verify == False 50 | assert accounts_conf.auth == 'basic' 51 | assert accounts_conf.user == 'testman' 52 | 53 | def assert_davical(accounts_conf): 54 | assert accounts_conf.write_support == True 55 | assert accounts_conf.resource == 'https://carddavcentral.com:4443/caldav.php/tester/abook/' 56 | assert accounts_conf.name == 'davical' 57 | assert accounts_conf.passwd == 'barfoo23' 58 | assert accounts_conf.verify == '/home/testman/.pycard/cacert.pem' 59 | assert accounts_conf.auth == 'digest' 60 | assert accounts_conf.user == 'tester' 61 | 62 | count = 0 63 | for one in conf.accounts: 64 | if one.name == 'work': 65 | assert_work(one) 66 | count += 1 67 | elif one.name == 'davical': 68 | assert_davical(one) 69 | count += 1 70 | elif one.name == 'work_no_verify': 71 | assert one.verify == True 72 | count += 1 73 | else: 74 | assert True == 'this should not be reached' 75 | assert count == 3 76 | 77 | 78 | def test_basic_debug(): 79 | """testing the basic configuration parser""" 80 | sys.argv = ['pycardsyncer', '-c', 81 | '{0}/assets/configs/base.conf'.format(basepath), 82 | '--debug'] 83 | conf_parser = ConfigurationParser('let\'s do a test', check_accounts=False) 84 | conf = conf_parser.parse() 85 | assert conf.debug == True 86 | assert conf.sqlite.path == '/home/testman/.pycard/abook.db' 87 | assert conf.filename.endswith('tests//assets/configs/base.conf') == True 88 | def assert_work(accounts_conf): 89 | assert accounts_conf.write_support == False 90 | assert accounts_conf.resource == 'http://test.com/abook/collection' 91 | assert accounts_conf.name == 'work' 92 | assert accounts_conf.passwd == 'foobar' 93 | assert accounts_conf.verify == False 94 | assert accounts_conf.auth == 'basic' 95 | assert accounts_conf.user == 'testman' 96 | 97 | def assert_davical(accounts_conf): 98 | assert accounts_conf.write_support == True 99 | assert accounts_conf.resource == 'https://carddavcentral.com:4443/caldav.php/tester/abook/' 100 | assert accounts_conf.name == 'davical' 101 | assert accounts_conf.passwd == 'barfoo23' 102 | assert accounts_conf.verify == '/home/testman/.pycard/cacert.pem' 103 | assert accounts_conf.auth == 'digest' 104 | assert accounts_conf.user == 'tester' 105 | 106 | count = 0 107 | for one in conf.accounts: 108 | if one.name == 'work': 109 | assert_work(one) 110 | count += 1 111 | elif one.name == 'davical': 112 | assert_davical(one) 113 | count += 1 114 | elif one.name == 'work_no_verify': 115 | assert one.verify == True 116 | count += 1 117 | else: 118 | assert True == 'this should not be reached' 119 | assert count == 3 120 | 121 | 122 | def test_sync_conf_parser(): 123 | """testing the basic configuration parser""" 124 | sys.argv = ['pycardsyncer', '-c', 125 | '{0}/assets/configs/base.conf'.format(basepath), 126 | '-a', 'work',] 127 | conf_parser = SyncConfigurationParser() 128 | conf = conf_parser.parse() 129 | assert conf.debug == False 130 | assert conf.sqlite.path == '/home/testman/.pycard/abook.db' 131 | assert conf.filename.endswith('tests//assets/configs/base.conf') == True 132 | assert conf.sync.accounts == set(['work']) 133 | def assert_work(accounts_conf): 134 | assert accounts_conf.write_support == False 135 | assert accounts_conf.resource == 'http://test.com/abook/collection/' 136 | assert accounts_conf.name == 'work' 137 | assert accounts_conf.passwd == 'foobar' 138 | assert accounts_conf.verify == False 139 | assert accounts_conf.auth == 'basic' 140 | assert accounts_conf.user == 'testman' 141 | 142 | def assert_davical(accounts_conf): 143 | assert accounts_conf.write_support == True 144 | assert accounts_conf.resource == 'https://carddavcentral.com:4443/caldav.php/tester/abook/' 145 | assert accounts_conf.name == 'davical' 146 | assert accounts_conf.passwd == 'barfoo23' 147 | assert accounts_conf.verify == '/home/testman/.pycard/cacert.pem' 148 | assert accounts_conf.auth == 'digest' 149 | assert accounts_conf.user == 'tester' 150 | 151 | count = 0 152 | for one in conf.accounts: 153 | if one.name == 'work': 154 | assert_work(one) 155 | count += 1 156 | elif one.name == 'davical': 157 | assert_davical(one) 158 | count += 1 159 | elif one.name == 'work_no_verify': 160 | assert one.verify == True 161 | count += 1 162 | else: 163 | assert True == 'this should not be reached' 164 | assert count == 3 165 | -------------------------------------------------------------------------------- /tests/vagrant/README.rst: -------------------------------------------------------------------------------- 1 | This vagrant box file can be used for testing pycarddav. It contains davical in 2 | version 1.0.2 on an ubuntu precise 32 bit. There have been two accounts 3 | configured, one (hanz) comes preloaded with all current test vcards (5), the 4 | other (lenna) has no card in its address book. 5 | 6 | use it like this:: 7 | 8 | $ rm Vagrantfile 9 | $ vagrant box add pycarddav_box http://lostpackets.de/pycarddav/vagrant/package.box 10 | $ vagrant init pycarddav_box 11 | 12 | $ py.test test_carddav.py 13 | 14 | 15 | the usernames/passwords are the following: 16 | 17 | * davicall admin password: XPPVDzQc 18 | * username: hanz password: foobar url: http://localhost:8080/davical/caldav.php/hanz/addresses/ 19 | * username: lenna password: test url: http://localhost:8080//davical/caldav.php/lenna/addresses/ 20 | 21 | 22 | Vagrantfile and Vagrantfile.pkg were used to create this package (and might be 23 | of some use later) and were therefore included. 24 | 25 | I tried bootstrapping the box with puppet (but failed), so there might be some 26 | puppet leftovers (but it works anyway). 27 | -------------------------------------------------------------------------------- /tests/vagrant/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant::Config.run do |config| 5 | # All Vagrant configuration is done here. The most common configuration 6 | # options are documented and commented below. For a complete reference, 7 | # please see the online documentation at vagrantup.com. 8 | 9 | # Every Vagrant virtual environment requires a box to build off of. 10 | config.vm.box = "pycarddav_box" 11 | 12 | # The url from where the 'config.vm.box' box will be fetched if it 13 | # doesn't already exist on the user's system. 14 | # config.vm.box_url = "http://domain.com/path/to/above.box" 15 | 16 | # Boot with a GUI so you can see the screen. (Default is headless) 17 | # config.vm.boot_mode = :gui 18 | 19 | # Assign this VM to a host-only network IP, allowing you to access it 20 | # via the IP. Host-only networks can talk to the host machine as well as 21 | # any other machines on the same network, but cannot be accessed (through this 22 | # network interface) by any external networks. 23 | # config.vm.network :hostonly, "192.168.33.10" 24 | 25 | # Assign this VM to a bridged network, allowing you to connect directly to a 26 | # network using the host's network device. This makes the VM appear as another 27 | # physical device on your network. 28 | # config.vm.network :bridged 29 | 30 | # Forward a port from the guest to the host, which allows for outside 31 | # computers to access the VM, whereas host only networking does not. 32 | config.vm.forward_port 80, 8080 33 | 34 | # Share an additional folder to the guest VM. The first argument is 35 | # an identifier, the second is the path on the guest to mount the 36 | # folder, and the third is the path on the host to the actual folder. 37 | # config.vm.share_folder "v-data", "/vagrant_data", "../data" 38 | 39 | # Enable provisioning with Puppet stand alone. Puppet manifests 40 | # are contained in a directory path relative to this Vagrantfile. 41 | # You will need to create the manifests directory and a manifest in 42 | # the file pycarddav_box.pp in the manifests_path directory. 43 | # 44 | # An example Puppet manifest to provision the message of the day: 45 | # 46 | # # group { "puppet": 47 | # # ensure => "present", 48 | # # } 49 | # # 50 | # # File { owner => 0, group => 0, mode => 0644 } 51 | # # 52 | # # file { '/etc/motd': 53 | # # content => "Welcome to your Vagrant-built virtual machine! 54 | # # Managed by Puppet.\n" 55 | # # } 56 | # 57 | # config.vm.provision :puppet do |puppet| 58 | # puppet.manifests_path = "manifests" 59 | # puppet.manifest_file = "pycarddav_box.pp" 60 | # end 61 | 62 | # Enable provisioning with chef solo, specifying a cookbooks path, roles 63 | # path, and data_bags path (all relative to this Vagrantfile), and adding 64 | # some recipes and/or roles. 65 | # 66 | # config.vm.provision :chef_solo do |chef| 67 | # chef.cookbooks_path = "../my-recipes/cookbooks" 68 | # chef.roles_path = "../my-recipes/roles" 69 | # chef.data_bags_path = "../my-recipes/data_bags" 70 | # chef.add_recipe "mysql" 71 | # chef.add_role "web" 72 | # 73 | # # You may also specify custom JSON attributes: 74 | # chef.json = { :mysql_password => "foo" } 75 | # end 76 | 77 | # Enable provisioning with chef server, specifying the chef server URL, 78 | # and the path to the validation key (relative to this Vagrantfile). 79 | # 80 | # The Opscode Platform uses HTTPS. Substitute your organization for 81 | # ORGNAME in the URL and validation key. 82 | # 83 | # If you have your own Chef Server, use the appropriate URL, which may be 84 | # HTTP instead of HTTPS depending on your configuration. Also change the 85 | # validation key to validation.pem. 86 | # 87 | # config.vm.provision :chef_client do |chef| 88 | # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" 89 | # chef.validation_key_path = "ORGNAME-validator.pem" 90 | # end 91 | # 92 | # If you're using the Opscode platform, your validator client is 93 | # ORGNAME-validator, replacing ORGNAME with your organization name. 94 | # 95 | # IF you have your own Chef Server, the default validation client name is 96 | # chef-validator, unless you changed the configuration. 97 | # 98 | # chef.validation_client_name = "ORGNAME-validator" 99 | end 100 | -------------------------------------------------------------------------------- /tests/vagrant/Vagrantfile.pkg: -------------------------------------------------------------------------------- 1 | Vagrant::Config.run do |config| 2 | # Forward apache and ssh 3 | config.vm.forward_port 80, 8080 4 | config.vm.forward_port 22, 2222 5 | end 6 | -------------------------------------------------------------------------------- /tests/vagrant/test_carddav.py: -------------------------------------------------------------------------------- 1 | # vim: set fileencoding=utf-8 : 2 | import vagrant 3 | import pytest 4 | import pycarddav.carddav as carddav 5 | 6 | HANZ_BASE = 'http://localhost:8080/davical/caldav.php/hanz/addresses/' 7 | LENNA_BASE = 'http://localhost:8080/davical/caldav.php/lenna/addresses/' 8 | 9 | 10 | def test_url_does_not_exist(): 11 | vbox = vagrant.Vagrant() 12 | vbox.up() 13 | with pytest.raises(carddav.requests.exceptions.HTTPError): 14 | carddav.PyCardDAV('http://localhost:8080/doesnotexist/') 15 | 16 | 17 | def test_no_auth(): 18 | vbox = vagrant.Vagrant() 19 | vbox.up() 20 | with pytest.raises(Exception): 21 | carddav.PyCardDAV(HANZ_BASE) 22 | 23 | 24 | def test_basic_auth(): 25 | vbox = vagrant.Vagrant() 26 | vbox.up() 27 | syncer = carddav.PyCardDAV(LENNA_BASE, user='lenna', passwd='test') 28 | abook = syncer.get_abook() 29 | assert abook == dict() 30 | --------------------------------------------------------------------------------