diff --git a/docs/supported.rst b/docs/supported.rst index a1bc1b1..cfe46b9 100644 --- a/docs/supported.rst +++ b/docs/supported.rst @@ -207,5 +207,28 @@ Vdirsyncer is continuously tested against the latest version of Baikal_. Google ------ -Vdirsyncer doesn't currently support Google accounts fully. For possible -solutions see :gh:`202` and :gh:`8`. +Using vdirsyncer with Google Calendar is possible, but it is not tested +frequently. + +:: + + [storage cal] + type = caldav + url = https://apidata.googleusercontent.com/caldav/v2/ + auth = oauth2_google + + [storage card] + type = carddav + url = https://www.googleapis.com/carddav/v1/principals/EMAIL/lists/default + auth = oauth2_google + +At first run you will be asked to authorize application for google account +access. Simply follow the instructions. You'll be asked to modify configuration +file (save `refresh_token` as a password). + +- Google's CardDav implementation is very limited, may lead to data loss, use + with care. +- You can select which calendars to sync on + `CalDav settings page `_ + +For more information see :gh:`202` and :gh:`8`. diff --git a/setup.py b/setup.py index 7375077..f7a558e 100644 --- a/setup.py +++ b/setup.py @@ -80,7 +80,8 @@ setup( }, install_requires=requirements, extras_require={ - 'remotestorage': ['requests-oauthlib'] + 'remotestorage': ['requests-oauthlib'], + 'oauth2': ['requests-oauthlib'], }, cmdclass={ 'minimal_requirements': PrintRequirements diff --git a/vdirsyncer/storage/dav.py b/vdirsyncer/storage/dav.py index 5a2d109..c863146 100644 --- a/vdirsyncer/storage/dav.py +++ b/vdirsyncer/storage/dav.py @@ -3,6 +3,8 @@ import datetime import logging +import click + from lxml import etree import requests @@ -14,6 +16,15 @@ from .http import HTTP_STORAGE_PARAMETERS, USERAGENT, prepare_auth, \ from .. import exceptions, utils from ..utils.compat import text_type, to_native +OAUTH2_GOOGLE_TOKEN_URL = 'https://accounts.google.com/o/oauth2/v2/auth' +OAUTH2_GOOGLE_REFRESH_URL = 'https://www.googleapis.com/oauth2/v4/token' +have_oauth2 = True +try: + from requests_oauthlib import OAuth2Session + oauth2_client_id = 'FIXME' + oauth2_client_secret = 'FIXME' +except ImportError: + have_oauth2 = False dav_logger = logging.getLogger(__name__) @@ -301,7 +312,6 @@ class DavSession(object): useragent=USERAGENT, verify_fingerprint=None, auth_cert=None): self._settings = { - 'auth': prepare_auth(auth, username, password), 'cert': prepare_client_cert(auth_cert), } self._settings.update(prepare_verify(verify, verify_fingerprint)) @@ -310,6 +320,21 @@ class DavSession(object): self.url = url.rstrip('/') + '/' self.parsed_url = utils.compat.urlparse.urlparse(self.url) self._session = None + self._token = None + self._use_oauth2_google = False + if auth == 'oauth2_google': + if not have_oauth2: + raise exceptions.UserError("requests-oauthlib not installed") + if password: + self._token = { + 'refresh_token': password, + # Will be derived from refresh_token + 'access_token': 'dummy', + 'expires_in': -30 + } + self._use_oauth2_google = True + else: + self._settings['auth'] = prepare_auth(auth, username, password) def request(self, method, path, **kwargs): url = self.url @@ -317,6 +342,44 @@ class DavSession(object): url = utils.compat.urlparse.urljoin(self.url, path) if self._session is None: self._session = requests.session() + if self._use_oauth2_google: + self._session = OAuth2Session( + client_id=oauth2_client_id, + token=self._token, + redirect_uri='urn:ietf:wg:oauth:2.0:oob', + scope=['https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/carddav'], + auto_refresh_url=OAUTH2_GOOGLE_REFRESH_URL, + auto_refresh_kwargs={ + 'client_id': oauth2_client_id, + 'client_secret': oauth2_client_secret, + }, + token_updater=lambda x: None + ) + if not self._token: + authorization_url, state = self._session.authorization_url( + OAUTH2_GOOGLE_TOKEN_URL, + # access_type and approval_prompt are Google specific + # extra parameters. + access_type="offline", approval_prompt="force") + click.echo('Opening {} ...'.format(authorization_url)) + try: + utils.open_graphical_browser(authorization_url) + except Exception as e: + dav_logger.warning(str(e)) + + click.echo("Follow the instructions on the page.") + code = click.prompt("Paste obtained code") + self._token = self._session.fetch_token( + OAUTH2_GOOGLE_REFRESH_URL, + code=code, + # Google specific extra parameter used for client + # authentication + client_secret=oauth2_client_secret, + ) + raise exceptions.UserError( + "Set the following token in a password field: {}". + format(self._token['refresh_token'])) more = dict(self._settings) more.update(kwargs) diff --git a/vdirsyncer/storage/http.py b/vdirsyncer/storage/http.py index da5ff07..8d04684 100644 --- a/vdirsyncer/storage/http.py +++ b/vdirsyncer/storage/http.py @@ -82,7 +82,10 @@ HTTP_STORAGE_PARAMETERS = ''' information. :param auth: Optional. Either ``basic``, ``digest`` or ``guess``. Default ``guess``. If you know yours, consider setting it explicitly for - performance. + performance. For caldav and carddav, additionaly ``oauth2_google`` is + supported. ``password`` setting should point a file for OAuth2 token + storage (directory must already exists, but file itself will be created + automatically). :param auth_cert: Optional. Either a path to a certificate with a client certificate and the key or a list of paths to the files with them. :param useragent: Default ``vdirsyncer``.