|
- #!/usr/bin/env python
- #-------------------------------------------------------------------------------
- # Copyright (c) 2013 by Lukasz Janyst <ljanyst@buggybrain.net>
- #
- # Permission to use, copy, modify, and/or distribute this software for any
- # purpose with or without fee is hereby granted, provided that the above
- # copyright notice and this permission notice appear in all copies.
- #
- # THE SOFTWARE IS PROVIDED 'AS IS' AND THE AUTHOR DISCLAIMS ALL WARRANTIES
- # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
- # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
- # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
- # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
- # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
- # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
- #-------------------------------------------------------------------------------
-
-
-
-
-
- import sys
- import uuid
- import getopt
- import getpass
- #import carddav # Copied below due to import error
- import vobject
- import os
- import vcfpy
- import re
- import json
- from collections import OrderedDict
-
-
- DEBUG = True
-
- #-------------------------------------------------------------------------------
- # Fix FN
- #-------------------------------------------------------------------------------
- def fixFN( url, filename, user, passwd, auth, verify ):
- print ('[debug] Editing at', url, '...')
- print ('[debug] Listing the addressbook...')
- dav = PyCardDAV( url, user=user, passwd=passwd, auth=auth,
- write_support=True, verify=verify )
- abook = dav.get_abook()
- nCards = len( abook.keys() )
- print ('[debug] Found', nCards, 'cards.')
-
- curr = 1
- for href, etag in abook.items():
- print ("\r[debug] Processing", curr, "of", nCards,)
- sys.stdout.flush()
- curr += 1
- card = dav.get_vcard( href )
- card = card.split( '\r\n' )
-
- cardFixed = []
- for l in card:
- if not l.startswith( 'FN:' ):
- cardFixed.append( l )
- cardFixed = '\r\n'.join( cardFixed )
-
- c = vobject.readOne( cardFixed )
-
- n = [c.n.value.prefix, c.n.value.given, c.n.value.additional,
- c.n.value.family, c.n.value.suffix]
- name = ''
- for part in n:
- if part:
- name += part + ' '
- name = name.strip()
-
- if not hasattr( c, 'fn' ):
- c.add('fn')
- c.fn.value = name
-
- try:
- dav.update_vcard( c.serialize().decode( 'utf-8' ), href, etag )
- except Exception as e:
- print ('')
- raise
- print ('')
- print ('[debug] All updated')
-
- #-------------------------------------------------------------------------------
- # Download
- #-------------------------------------------------------------------------------
- def download( url, filename, user, passwd, auth, verify):
- if DEBUG: print ('[debug] Downloading from', url, 'to', filename, '...')
- if DEBUG: print ('[debug] Downloading the addressbook...')
- try:
- dav = PyCardDAV( url, user=user, passwd=passwd, auth=auth,
- verify=verify )
- abook = dav.get_abook()
- nCards = len( abook.keys() )
- if DEBUG: print ('[debug] Found', nCards, 'cards.')
-
- f = open( filename, 'wb' )
-
- curr = 1
- for href, etag in abook.items():
- if DEBUG: print ("\r[debug] Fetching", curr, "of", nCards,)
- sys.stdout.flush()
- curr += 1
- card = dav.get_vcard( href )
- #f.write( str(card.decode("utf-8")) )
- if DEBUG: print ("[debug] Writing on "+filename)
- #print ("[debug] CARD: "+card)
- f.write( card )
- #f.write( '\n' )
- f.close()
- if DEBUG: print ('[debug] All saved to:', filename)
- except Exception as e:
- if DEBUG: print(e)
-
- #-------------------------------------------------------------------------------
- # Read
- #-------------------------------------------------------------------------------
- def read( url, filename, user, passwd, auth, verify):
-
- """
- Example of READ use:
- python3.6 carddav-util.py --read --user=digiovine --passwd=Digiovine1! --file=test.vcf --url=https://mail.afasystems.it/drive/remote.php/dav/addressbooks/users/digiovine/rubrica-prova/
-
- """
- if DEBUG: print("[debug] ########## STARTING READ ##########")
- cards_response = {"contacts": []}
- if not filename:
- filename = _filename_from_url(url)
- download ( url, filename, user, passwd, auth, verify)
- if DEBUG: print("[debug] Download completed")
-
- f = open( filename, 'r' )
- cards = []
- for card in vobject.readComponents( f, validate=True ):
- cards.append( card )
- nCards = len(cards)
- if DEBUG: print ('[debug] Successfully read and validated', nCards, 'entries')
-
- for card in cards:
- if DEBUG: print("\n")
- if DEBUG: print("[debug] VCARD:")
- if DEBUG: print(card)
- if DEBUG: print("\n")
- cards_response['contacts'].append(_convertVcardToJson(card))
- f.close()
-
-
- if os.path.exists(filename):
- os.remove(filename)
- if DEBUG: print("File '"+filename+"' has been deleted")
- else:
- if DEBUG: print("The file does not exist")
- if DEBUG: print ('[debug] All done')
- #print (json.dumps(cards_response, indent=4, sort_keys=True))
- if DEBUG: print(cards_response)
- return cards_response
-
- def _filename_from_url(url):
- """
- This method takes in input a cardDav addressbook url,
- extract from the url the username and the addressbookName and then
- returns a string built with pattern: {username}_{addressbookName}
- """
- try:
- splitted_url = url.split("users/")[1]
- username = splitted_url.split("/")[0]
- addressbookName = splitted_url.split("/")[1]
- filename = username + "_" + addressbookName + ".vcf"
- except:
- filename = "addressbook_temp_generic_name.vcf"
- return filename
-
- def _convertVcardToJson(vcard):
- '''
- Takes in input a single vcard in vobject format and
- returns a json object of type:
-
- '''
- try:
- fn = _clean_vobject_attribute(vcard.fn, 'fn')
- except:
- fn = ""
- try:
- tel = {}
- for phoneNumber in vcard.contents['tel']:
- phoneNumber = str(phoneNumber)
- if "HOME" in phoneNumber and "VOICE" in phoneNumber:
- phoneKey = "homeVoice"
- elif "WORK" in phoneNumber and "VOICE" in phoneNumber:
- phoneKey = "workVoice"
- elif "HOME" in phoneNumber and "CELL" in phoneNumber:
- phoneKey = "personalMobile"
- elif "WORK" in phoneNumber and "CELL" in phoneNumber:
- phoneKey = "workMobile"
- else:
- phoneKey = "unknowkn"
- tel[phoneKey] = (_clean_vobject_attribute(phoneNumber, 'tel'))
- except:
- tel = []
- adr = _build_address_attribute(_clean_vobject_attribute(vcard.adr, 'adr'))
- try:
- email = _clean_vobject_attribute(vcard.email, 'email')
- except:
- email = ""
- try:
- categories = _clean_vobject_attribute(vcard.categories, 'categories')
- except:
- categories = []
- card_json = OrderedDict({
- "fn": fn,
- "tel": tel,
- "adr": adr,
- "email": email,
- "categories": categories,
- })
- if DEBUG: print("[DEBUG] Converted VCARD to JSON: ")
- if DEBUG: print(json.dumps(card_json, indent=4, sort_keys=True))
- return card_json
-
-
- def _clean_vobject_attribute(attribute, attr_type):
- attribute = str(attribute)
- # Common cleaning
- attribute = attribute.replace("{", "")
- attribute = attribute.replace("}", "")
- attribute = attribute.replace("<", "")
- attribute = attribute.replace(">", "")
- attribute = attribute.replace("TYPE", "")
- attribute = attribute.replace("VOICE", "")
- attribute = attribute.replace("[", "")
- attribute = attribute.replace("]", "")
- attribute = attribute.replace(":", "")
- attribute = attribute.replace("'", "")
- if attr_type == 'fn':
- # FN cleaning
- attribute = attribute.replace("FN", "")
- elif attr_type == 'tel':
- # TEL cleaning
- attribute = attribute.replace("TEL", "")
- attribute = attribute.replace("CELL", "")
- attribute = attribute.replace("WORK", "")
- attribute = attribute.replace("HOME", "")
- attribute = attribute.replace(",", "")
- attribute = attribute.replace("\"", "")
- attribute = attribute.replace(" ", "")
- elif attr_type == 'adr':
- # ADR cleaning
- attribute = attribute.replace("HOME", "")
- attribute = attribute.replace("ADR", "")
- attribute = attribute[1:]
- elif attr_type == 'email':
- # EMAIL cleaning
- attribute = attribute.replace("HOME", "")
- attribute = attribute.replace("EMAIL", "")
- attribute = attribute[1:]
- elif attr_type == 'categories':
- # CATEGORIES cleaning
- attribute = attribute.replace("CATEGORIES", "")
- attribute = attribute.split(", ")
- return attribute
-
- def _build_address_attribute(addr_string):
-
- casella_postale = ""
- indirizzo = ""
- indirizzo_esteso = ""
- citta_regione_cap = ""
- stato = ""
-
- addr_array = addr_string.split("\n")
- if len(addr_array) > 4:
- if(len(addr_array)>0):
- casella_postale = addr_array[0]
- if(len(addr_array)>1):
- indirizzo_esteso = addr_array[1]
- if indirizzo == ", ":
- indirizzo = ""
- if(len(addr_array)>2):
- indirizzo = addr_array[2]
- if indirizzo == ", ":
- indirizzo = ""
- if(len(addr_array)>3):
- citta_regione_cap = addr_array[3]
- if(len(addr_array)>4):
- stato = addr_array[4]
- addr_json = {
- "casellaPostale": casella_postale,
- "indirizzo": indirizzo,
- "indirizzoEsteso": indirizzo_esteso,
- "cittaRegioneCAP": citta_regione_cap,
- "stato": stato
- }
- return addr_json
-
- #-------------------------------------------------------------------------------
- # Upload
- #-------------------------------------------------------------------------------
- def upload( url, filename, user, passwd, auth, verify ):
- if not url.endswith( '/' ):
- url += '/'
-
- print ('[debug] Uploading from', filename, 'to', url, '...')
-
- print ('[debug] Processing cards in', filename, '...')
- f = open( filename, 'r' )
- cards = []
- for card in vobject.readComponents( f, validate=True ):
- cards.append( card )
- nCards = len(cards)
- print ('[debug] Successfuly read and validated', nCards, 'entries')
-
- print ('[debug] Connecting to', url, '...')
- dav = PyCardDAV( url, user=user, passwd=passwd, auth=auth,
- write_support=True, verify=verify )
-
- curr = 1
- for card in cards:
- print ("\r[debug] Uploading", curr, "of", nCards,)
- sys.stdout.flush()
- curr += 1
-
- if hasattr(card, 'prodid' ):
- del card.prodid
-
- if not hasattr( card, 'uid' ):
- card.add('uid')
- card.uid.value = str( uuid.uuid4() )
- try:
- dav.upload_new_card( card.serialize().decode('utf-8') )
- except Exception as e:
- print ('')
- raise
- print ('')
- f.close()
- print ('[debug] All done')
-
- def makeVcard(first_name, last_name, company, title, phone, address, email):
- address_formatted = ';'.join([p.strip() for p in address.split(',')])
- return [
- 'BEGIN:VCARD',
- 'VERSION:2.1',
- f'N:{last_name};{first_name}',
- f'FN:{first_name} {last_name}',
- f'ORG:{company}',
- f'TITLE:{title}',
- f'EMAIL;PREF;INTERNET:{email}',
- f'TEL;WORK;VOICE:{phone}',
- f'ADR;WORK;PREF:;;{address_formatted}',
- f'REV:1',
- 'END:VCARD'
- ]
-
- def writeVcard(filename, vcard):
- with open(filename, 'w') as f:
- f.writelines([l + '\n' for l in vcard])
-
-
- #-------------------------------------------------------------------------------
- # Print help
- #-------------------------------------------------------------------------------
- def printHelp():
- print( 'carddav-util.py [options]' )
- print( ' --url=http://your.addressbook.com CardDAV addressbook ' )
- print( ' --file=local.vcf local vCard file ' )
- print( ' --user=username username ' )
- print( ' --passwd=password password, if absent will ' )
- print( ' prompt for it in the console ' )
- print( ' --download copy server -> file ' )
- print( ' --upload copy file -> server ' )
- print( ' --fixfn regenerate the FN tag ' )
- print( ' --digest use digest authentication ' )
- print( ' --no-cert-verify skip certificate verification ' )
- print( ' --help this help message ' )
-
-
- #-------------------------------------------------------------------------------
- # Run the show
- #-------------------------------------------------------------------------------
- def main():
- try:
- params = ['url=', 'file=', 'download', 'upload', 'read', 'help',
- 'user=', 'passwd=', 'digest', 'no-cert-verify', 'fixfn']
- optlist, args = getopt.getopt( sys.argv[1:], '', params )
- except getopt.GetoptError as e:
- print ('[!]', e)
- return 1
-
- opts = dict(optlist)
- if '--help' in opts or not opts:
- printHelp()
- return 0
-
- if '--upload' in opts and '--download' in opts and '--fixfn' in opts:
- print ('[!] You can only choose one action at a time')
- return 2
-
- if '--url' not in opts:
- print ('[!] You must specify the url')
- return 3
-
- url = opts['--url']
- try:
- filename = opts['--file']
- except:
- filename = ""
-
- user = None
- passwd = None
- auth = 'basic'
- verify = True
-
- if '--digest' in opts:
- auth = 'digest'
-
- if '--no-cert-verify' in opts:
- verify = False
-
- if '--user' in opts:
- user = opts['--user']
- if '--passwd' in opts:
- passwd = opts['--passwd']
- else:
- passwd = getpass.getpass( user+'\'s password (won\'t be echoed): ')
-
- commandMap = {'--upload': upload, '--download': download, '--read': read, '--fixfn': fixFN}
- for command in commandMap:
- if command in opts:
- i = 0
- try:
- i = commandMap[command]( url, filename, user, passwd, auth, verify )
- except Exception as e:
- print ('[!]', e)
-
- if __name__ == '__main__':
- sys.exit(main())
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- #!/usr/bin/env python
- # vim: set ts=4 sw=4 expandtab sts=4:
- # Copyright (c) 2011-2013 Christian Geier & contributors
- #
- # Permission is hereby granted, free of charge, to any person obtaining
- # a copy of this software and associated documentation files (the
- # "Software"), to deal in the Software without restriction, including
- # without limitation the rights to use, copy, modify, merge, publish,
- # distribute, sublicense, and/or sell copies of the Software, and to
- # permit persons to whom the Software is furnished to do so, subject to
- # the following conditions:
- #
- # The above copyright notice and this permission notice shall be
- # included in all copies or substantial portions of the Software.
- #
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
- #-------------------------------------------------------------------------------
- # Lukasz Janyst:
- #
- # lxml encoding issue:
- # * Remove '<?xml version="1.0" encoding="utf-8"?>' header from responses
- # to prevent etree errors
- #
- # requests-0.8.2:
- # * Remove the verify ssl flag - caused exception
- # * Add own raise_for_status for more meaningful error messages
- # * Fix digest auth
- #-------------------------------------------------------------------------------
-
- """
- contains the class PyCardDAV and some associated functions and definitions
- """
-
- from collections import namedtuple
- import requests
- import sys
- import urllib.parse as urlparse
- import logging
- import lxml.etree as ET
- import string
-
- def raise_for_status( resp ):
- if 400 <= resp.status_code < 500 or 500 <= resp.status_code < 600:
- msg = 'Error code: ' + str(resp.status_code) + '\n'
- msg += resp.content
- raise requests.exceptions.HTTPError( msg )
-
- def get_random_href():
- """returns a random href"""
- import random
- tmp_list = list()
- for _ in xrange(3):
- rand_number = random.randint(0, 0x100000000)
- tmp_list.append("{0:x}".format(rand_number))
- return "-".join(tmp_list).upper()
-
-
- DAVICAL = 'davical'
- SABREDAV = 'sabredav'
- UNKNOWN = 'unknown server'
-
-
- class UploadFailed(Exception):
- """uploading the card failed"""
- pass
-
-
- class PyCardDAV(object):
- """class for interacting with a CardDAV server
- Since PyCardDAV relies heavily on Requests [1] its SSL verification is also
- shared by PyCardDAV [2]. For now, only the *verify* keyword is exposed
- through PyCardDAV.
- [1] http://docs.python-requests.org/
- [2] http://docs.python-requests.org/en/latest/user/advanced/
- raises:
- requests.exceptions.SSLError
- requests.exceptions.ConnectionError
- more requests.exceptions depending on the actual error
- Exception (shame on me)
- """
-
- def __init__(self, resource, debug='', user='', passwd='',
- verify=True, write_support=False, auth='basic'):
- #shutup url3
- urllog = logging.getLogger('requests.packages.urllib3.connectionpool')
- urllog.setLevel(logging.CRITICAL)
-
- split_url = urlparse.urlparse(resource)
- url_tuple = namedtuple('url', 'resource base path')
- self.url = url_tuple(resource,
- split_url.scheme + '://' + split_url.netloc,
- split_url.path)
- self.debug = debug
- self.session = requests.session()
- self.write_support = write_support
- self._settings = {'verify': verify}
- if auth == 'basic':
- self._settings['auth'] = (user, passwd,)
- if auth == 'digest':
- from requests.auth import HTTPDigestAuth
- self._settings['auth'] = HTTPDigestAuth(user, passwd)
- self._default_headers = {"User-Agent": "pyCardDAV"}
- response = self.session.request('PROPFIND', resource,
- headers=self.headers,
- **self._settings)
- raise_for_status( response ) #raises error on not 2XX HTTP status code
-
-
- @property
- def verify(self):
- """gets verify from settings dict"""
- return self._settings['verify']
-
- @verify.setter
- def verify(self, verify):
- """set verify"""
- self._settings['verify'] = verify
-
- @property
- def headers(self):
- return dict(self._default_headers)
-
- def _check_write_support(self):
- """checks if user really wants his data destroyed"""
- if not self.write_support:
- sys.stderr.write("Sorry, no write support for you. Please check "
- "the documentation.\n")
- sys.exit(1)
-
- def _detect_server(self):
- """detects CardDAV server type
- currently supports davical and sabredav (same as owncloud)
- :rtype: string "davical" or "sabredav"
- """
- response = requests.request('OPTIONS',
- self.url.base,
- headers=self.header)
- if "X-Sabre-Version" in response.headers:
- server = SABREDAV
- elif "X-DAViCal-Version" in response.headers:
- server = DAVICAL
- else:
- server = UNKNOWN
- logging.info(server + " detected")
- return server
-
- def get_abook(self):
- """does the propfind and processes what it returns
- :rtype: list of hrefs to vcards
- """
- xml = self._get_xml_props()
- abook = self._process_xml_props(xml)
- return abook
-
- def get_vcard(self, href):
- """
- pulls vcard from server
- :returns: vcard
- :rtype: string
- """
- response = self.session.get(self.url.base + href,
- headers=self.headers,
- **self._settings)
- raise_for_status( response )
- return response.content
-
- def update_vcard(self, card, href, etag):
- """
- pushes changed vcard to the server
- card: vcard as unicode string
- etag: str or None, if this is set to a string, card is only updated if
- remote etag matches. If etag = None the update is forced anyway
- """
- # TODO what happens if etag does not match?
- self._check_write_support()
- remotepath = str(self.url.base + href)
- headers = self.headers
- headers['content-type'] = 'text/vcard'
- if etag is not None:
- headers['If-Match'] = etag
- self.session.put(remotepath, data=card.encode('utf-8'), headers=headers,
- **self._settings)
-
- def delete_vcard(self, href, etag):
- """deletes vcard from server
- deletes the resource at href if etag matches,
- if etag=None delete anyway
- :param href: href of card to be deleted
- :type href: str()
- :param etag: etag of that card, if None card is always deleted
- :type href: str()
- :returns: nothing
- """
- # TODO: what happens if etag does not match, url does not exist etc ?
- self._check_write_support()
- remotepath = str(self.url.base + href)
- headers = self.headers
- headers['content-type'] = 'text/vcard'
- if etag is not None:
- headers['If-Match'] = etag
- result = self.session.delete(remotepath,
- headers=headers,
- **self._settings)
- raise_for_status( response )
-
- def upload_new_card(self, card):
- """
- upload new card to the server
- :param card: vcard to be uploaded
- :type card: unicode
- :rtype: tuple of string (path of the vcard on the server) and etag of
- new card (string or None)
- """
- self._check_write_support()
- card = card.encode('utf-8')
- for _ in range(0, 5):
- rand_string = get_random_href()
- remotepath = str(self.url.resource + rand_string + ".vcf")
- headers = self.headers
- headers['content-type'] = 'text/vcard'
- headers['If-None-Match'] = '*'
- response = requests.put(remotepath, data=card, headers=headers,
- **self._settings)
- if response.ok:
- parsed_url = urlparse.urlparse(remotepath)
-
- if 'etag' not in response.headers:
- etag = ''
- else:
- etag = response.headers['etag']
-
- return (parsed_url.path, etag)
- raise_for_status( response )
-
- def _get_xml_props(self):
- """PROPFIND method
- gets the xml file with all vcard hrefs
- :rtype: str() (an xml file)
- """
- headers = self.headers
- headers['Depth'] = '1'
- response = self.session.request('PROPFIND',
- self.url.resource,
- headers=headers,
- **self._settings)
- raise_for_status( response )
- if response.headers['DAV'].count('addressbook') == 0:
- raise Exception("URL is not a CardDAV resource")
-
- return response.content
-
- @classmethod
- def _process_xml_props(cls, xml):
- """processes the xml from PROPFIND, listing all vcard hrefs
- :param xml: the xml file
- :type xml: str()
- :rtype: dict() key: href, value: etag
- """
- #xml.replace('<?xml version="1.0" encoding="utf-8"?>', '')
- namespace = "{DAV:}"
-
- element = ET.XML(xml)
- abook = dict()
- for response in element.iterchildren():
- if (response.tag == namespace + "response"):
- href = ""
- etag = ""
- insert = False
- for refprop in response.iterchildren():
- if (refprop.tag == namespace + "href"):
- href = refprop.text
- for prop in refprop.iterchildren():
- for props in prop.iterchildren():
- if (props.tag == namespace + "getcontenttype" and
- (props.text == "text/vcard" or
- props.text == "text/vcard; charset=utf-8" or
- props.text == "text/x-vcard" or
- props.text == "text/x-vcard; charset=utf-8")):
- insert = True
- if (props.tag == namespace + "getetag"):
- etag = props.text
- if insert:
- abook[href] = etag
- return abook
|