Example #1
0
    def watch_calendar_list(self, account):
        """
        Subscribe to google push notifications for the calendar list.

        Returns the expiration of the notification channel (as a
        Unix timestamp in ms)

        Raises an OAuthError if no credentials are authorized to
        set up push notifications for this account.

        Raises an AccessNotEnabled error if calendar sync is not enabled
        """
        token = self._get_access_token_for_push_notifications(account)
        receiving_url = CALENDAR_LIST_WEBHOOK_URL.format(
            urllib.quote(account.public_id))
        data = {
            "id": uuid.uuid4().hex,
            "type": "web_hook",
            "address": receiving_url,
        }
        headers = {'content-type': 'application/json'}
        r = requests.post(WATCH_CALENDARS_URL,
                          data=json.dumps(data),
                          headers=headers,
                          auth=OAuthRequestsWrapper(token))

        if r.status_code == 200:
            data = r.json()
            return data.get('expiration')
        else:
            self.handle_watch_errors(r)
            return None
Example #2
0
 def _search(self, search_query, limit):
     ret = requests.get(
         u'https://www.googleapis.com/gmail/v1/users/me/messages',
         params={
             'q': search_query,
             'maxResults': limit
         },
         auth=OAuthRequestsWrapper(self.auth_token))
     log.info('Gmail API search request completed',
              elapsed=ret.elapsed.total_seconds())
     if ret.status_code != 200:
         log.critical('HTTP error making search request',
                      account_id=self.account.id,
                      url=ret.url,
                      response=ret.content)
         raise SearchBackendException("Error issuing search request",
                                      503,
                                      server_error=ret.content)
     data = ret.json()
     if 'messages' not in data:
         return []
     # Note that the Gmail API returns g_msgids in hex format. So for
     # example the IMAP X-GM-MSGID 1438297078380071706 corresponds to
     # 13f5db9286538b1a in the API response we have here.
     return [int(m['id'], 16) for m in data['messages']]
Example #3
0
    def _search(self, search_query, limit):
        results = []

        params = dict(q=search_query, maxResults=limit)

        # Could have used while True: but I don't like infinite loops.
        for i in range(1, 10):
            ret = requests.get(
                u"https://www.googleapis.com/gmail/v1/users/me/messages",
                params=params,
                auth=OAuthRequestsWrapper(self.auth_token),
            )

            log.info(
                "Gmail API search request completed",
                elapsed=ret.elapsed.total_seconds(),
            )

            if ret.status_code != 200:
                log.critical(
                    "HTTP error making search request",
                    account_id=self.account.id,
                    url=ret.url,
                    response=ret.content,
                )
                raise SearchBackendException("Error issuing search request",
                                             503,
                                             server_error=ret.content)

            data = ret.json()

            if "messages" not in data:
                return results

            # Note that the Gmail API returns g_msgids in hex format. So for
            # example the IMAP X-GM-MSGID 1438297078380071706 corresponds to
            # 13f5db9286538b1a in the API response we have here.
            results = results + [int(m["id"], 16) for m in data["messages"]]

            if len(results) >= limit:
                return results[:limit]

            if "nextPageToken" not in data:
                return results
            else:
                # We don't have <limit> results and there's more to fetch ---
                # get them!
                params["pageToken"] = data["nextPageToken"]
                log.info("Getting next page of search results")
                continue

        # If we've been through the loop 10 times, it means we got a request
        # a crazy-high offset --- raise an error.
        log.error("Too many search results", query=search_query, limit=limit)

        raise SearchBackendException("Too many results", 400)
Example #4
0
    def watch_calendar(self, account, calendar):
        """
        Subscribe to google push notifications for a calendar.

        Returns the expiration of the notification channel (as a
        Unix timestamp in ms)

        Raises an OAuthError if no credentials are authorized to
        set up push notifications for this account.

        Raises an AccessNotEnabled error if calendar sync is not enabled

        Raises an HTTPError if google gives us a 404 (which implies the
        calendar was deleted)

        """
        token = self._get_access_token_for_push_notifications(account)
        watch_url = WATCH_EVENTS_URL.format(urllib.quote(calendar.uid))
        receiving_url = EVENTS_LIST_WEHOOK_URL.format(
            urllib.quote(calendar.public_id))

        one_week = datetime.timedelta(weeks=1)
        in_a_week = datetime.datetime.utcnow() + one_week

        # * 1000 because google uses Unix timestamps with an ms precision.
        expiration_date = int(time.mktime(in_a_week.timetuple())) * 1000

        data = {
            "id": uuid.uuid4().hex,
            "type": "web_hook",
            "address": receiving_url,
            "expiration": expiration_date,
        }
        headers = {"content-type": "application/json"}
        try:
            r = requests.post(
                watch_url,
                data=json.dumps(data),
                headers=headers,
                auth=OAuthRequestsWrapper(token),
            )
        except requests.exceptions.SSLError:
            self.log.warning(
                "SSLError subscribing to Google push notifications",
                url=watch_url,
                exc_info=True,
            )
            return

        if r.status_code == 200:
            data = r.json()
            return data.get("expiration")
        else:
            # Handle error and return None
            self.handle_watch_errors(r)
Example #5
0
    def _get_resource_list(self, url, **params):
        """Handles response pagination."""
        token = self._get_access_token()
        items = []
        next_page_token = None
        params['showDeleted'] = True
        while True:
            if next_page_token is not None:
                params['pageToken'] = next_page_token
            try:
                r = requests.get(url, params=params,
                                 auth=OAuthRequestsWrapper(token))
                r.raise_for_status()
                data = r.json()
                items += data['items']
                next_page_token = data.get('nextPageToken')
                if next_page_token is None:
                    return items

            except requests.exceptions.SSLError:
                self.log.warning(
                    'SSLError making Google Calendar API request, retrying.',
                    url=url, exc_info=True)
                gevent.sleep(30 + random.randrange(0, 60))
                continue
            except requests.HTTPError:
                self.log.warning(
                    'HTTP error making Google Calendar API request', url=r.url,
                    response=r.content, status=r.status_code)
                if r.status_code == 401:
                    self.log.warning(
                        'Invalid access token; refreshing and retrying',
                        url=r.url, response=r.content, status=r.status_code)
                    token = self._get_access_token(force_refresh=True)
                    continue
                elif r.status_code in (500, 503):
                    log.warning('Backend error in calendar API; retrying')
                    gevent.sleep(30 + random.randrange(0, 60))
                    continue
                elif r.status_code == 403:
                    try:
                        reason = r.json()['error']['errors'][0]['reason']
                    except (KeyError, ValueError):
                        log.error("Couldn't parse API error response",
                                  response=r.content, status=r.status_code)
                        r.raise_for_status()
                    if reason == 'userRateLimitExceeded':
                        log.warning('API request was rate-limited; retrying')
                        gevent.sleep(30 + random.randrange(0, 60))
                        continue
                    elif reason == 'accessNotConfigured':
                        log.warning('API not enabled; returning empty result')
                        raise AccessNotEnabledError()
                # Unexpected error; raise.
                raise
Example #6
0
 def _make_event_request(self, method, calendar_uid, event_uid=None,
                         **kwargs):
     """ Makes a POST/PUT/DELETE request for a particular event. """
     event_uid = event_uid or ''
     url = 'https://www.googleapis.com/calendar/v3/' \
           'calendars/{}/events/{}'.format(urllib.quote(calendar_uid),
                                           urllib.quote(event_uid))
     token = self._get_access_token()
     response = requests.request(method, url,
                                 auth=OAuthRequestsWrapper(token),
                                 **kwargs)
     return response
Example #7
0
    def watch_calendar(self, account, calendar):
        """
        Subscribe to google push notifications for a calendar.

        Returns the expiration of the notification channel (as a
        Unix timestamp in ms)

        Raises an OAuthError if no credentials are authorized to
        set up push notifications for this account.

        Raises an AccessNotEnabled error if calendar sync is not enabled

        Raises an HTTPError if google gives us a 404 (which implies the
        calendar was deleted)
        """
        token = self._get_access_token_for_push_notifications(account)
        watch_url = WATCH_EVENTS_URL.format(urllib.quote(calendar.uid))
        receiving_url = EVENTS_LIST_WEHOOK_URL.format(
            urllib.quote(calendar.public_id))
        data = {
            "id": uuid.uuid4().hex,
            "type": "web_hook",
            "address": receiving_url,
        }
        headers = {'content-type': 'application/json'}
        try:
            r = requests.post(watch_url,
                              data=json.dumps(data),
                              headers=headers,
                              auth=OAuthRequestsWrapper(token))
        except requests.exceptions.SSLError:
            self.log.warning(
                'SSLError subscribing to Google push notifications',
                url=watch_url,
                exc_info=True)
            return

        if r.status_code == 200:
            data = r.json()
            return data.get('expiration')
        else:
            self.handle_watch_errors(r)
            return
Example #8
0
    def watch_calendar_list(self, account):
        """
        Subscribe to google push notifications for the calendar list.

        Returns the expiration of the notification channel (as a
        Unix timestamp in ms)

        Raises an OAuthError if no credentials are authorized to
        set up push notifications for this account.

        Raises an AccessNotEnabled error if calendar sync is not enabled

        """
        token = self._get_access_token_for_push_notifications(account)
        receiving_url = CALENDAR_LIST_WEBHOOK_URL.format(
            urllib.quote(account.public_id))

        one_week = datetime.timedelta(weeks=1)
        in_a_week = datetime.datetime.utcnow() + one_week

        # * 1000 because google uses Unix timestamps with an ms precision.
        expiration_date = int(time.mktime(in_a_week.timetuple())) * 1000

        data = {
            "id": uuid.uuid4().hex,
            "type": "web_hook",
            "address": receiving_url,
            "expiration": expiration_date,
        }
        headers = {"content-type": "application/json"}
        r = requests.post(
            WATCH_CALENDARS_URL,
            data=json.dumps(data),
            headers=headers,
            auth=OAuthRequestsWrapper(token),
        )

        if r.status_code == 200:
            data = r.json()
            return data.get("expiration")
        else:
            # Handle error and return None
            self.handle_watch_errors(r)
Example #9
0
def get_gmail_raw_contents(message):
    account = message.namespace.account
    auth_token = g_token_manager.get_token_for_email(account)

    # The Gmail API exposes the X-GM-MSGID field but encodes it
    # in hexadecimal.
    g_msgid = message.g_msgid

    if g_msgid is None:
        raise EmailDeletedException(
            "Couldn't find message on backend server. This is a permanent error."
        )

    if isinstance(g_msgid, basestring):
        g_msgid = int(g_msgid)

    hex_id = format(g_msgid, 'x')
    url = 'https://www.googleapis.com/gmail/v1/users/me/messages/{}?format=raw'.format(
        hex_id, 'x')
    r = requests.get(url, auth=OAuthRequestsWrapper(auth_token))

    if r.status_code != 200:
        log.error('Got an error when fetching raw email', r.status_code,
                  r.text)

    if r.status_code in [403, 429]:
        raise TemporaryEmailFetchException(
            "Temporary usage limit hit. Please try again.")
    if r.status_code == 404:
        raise EmailDeletedException(
            "Couldn't find message on backend server. This is a permanent error."
        )
    elif r.status_code >= 500 and r.status_code <= 599:
        raise TemporaryEmailFetchException(
            "Backend server error. Please try again in a few minutes.")

    data = r.json()
    raw = str(data['raw'])
    return base64.urlsafe_b64decode(raw + '=' * (4 - len(raw) % 4))