# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Functions to query billing info using the Google API."""

from apiclient.discovery import build
import credentials_utils
import log_utils

_LOG = log_utils.GetLogger('messagerecall.billing_info')


class BillingInfo(object):
    """Class to retrieve billing info.

  Uses http_utils to add error handling and retry using backoff.
  """
    def __init__(self, owner_email):
        """Initialize the billing client.

    Args:
      owner_email: String email address of the project admin.
    """
        self._http = credentials_utils.GetAuthorizedHttp(owner_email)
Пример #2
0
from models import recall_task
from models import sharded_counter
import recall_errors
import user_retriever
import view_utils
import webapp2

from google.appengine.api import apiproxy_stub_map
from google.appengine.api import runtime
from google.appengine.api.taskqueue import Error as TaskQueueError
from google.appengine.api.taskqueue import Queue
from google.appengine.api.taskqueue import Task
from google.appengine.ext import ndb


_LOG = log_utils.GetLogger('messagerecall.views')
_MONITOR_SLEEP_PERIOD_S = 10

# Write users to the db in batches to save time/rpcs.
_USER_PUT_MULTI_BATCH_SIZE = 100


def PartitionEmailPrefixes():
  """Divide the domain email namespace to allow concurrent tasks to search.

  The rules for gmail usernames are:
  a) Letters, numbers and . are allowed.
  b) The first character must be a letter or number.

  Returns:
    List of Strings which are the valid prefixes the user search tasks will
import sys

_MESSAGE_RECALL_DIR = os.path.dirname(os.path.abspath(__file__))


def _SetMessageRecallLibPath(sys_path, message_recall_dir):
    message_recall_lib_dir = os.path.join(message_recall_dir, 'lib')
    if message_recall_lib_dir not in sys_path:
        sys_path.append(message_recall_lib_dir)


_SetMessageRecallLibPath(sys.path, _MESSAGE_RECALL_DIR)

import log_utils  # pylint: disable=g-import-not-at-top

_LOG = log_utils.GetLogger('messagerecall.setup_path')

try:
    # pylint: disable=g-import-not-at-top, unused-import
    import apiclient
    from apiclient.discovery import build
    import httplib2
    import oauth2client
    from oauth2client.tools import run
    from oauth2client.client import SignedJwtAssertionCredentials
    # pylint: enable=g-import-not-at-top, unused-import
except ImportError as e:
    module_package_map = {
        'apiclient': 'google-api-python-client',
        'apiclient.discovery': 'google-api-python-client',
        'oauth2client.tools': 'oauth2client.tools',
Пример #4
0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Database models to track error reasons for reporting."""

import log_utils

from google.appengine.datastore.datastore_query import Cursor
from google.appengine.ext import ndb


_LOG = log_utils.GetLogger('messagerecall.models.error_reason')
_REASON_ROWS_FETCH_PAGE = 20


class ErrorReasonModel(ndb.Model):
  """Model to track errors in the various recall tasks."""

  recall_task_id = ndb.IntegerProperty(required=True)
  user_email = ndb.StringProperty()  # Empty if error before user activities.
  error_datetime = ndb.DateTimeProperty(required=True, auto_now_add=True)
  error_reason = ndb.StringProperty(required=True)  # Max 500 character length.

  @classmethod
  def FetchOneUIPageOfErrorsForTask(cls, task_key_urlsafe, urlsafe_cursor):
    """Utility to query and fetch all error reasons for UI pages.
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Database models for candidate domain users to check for message presence."""

import log_utils

from google.appengine.datastore.datastore_query import Cursor
from google.appengine.ext import ndb


_LOG = log_utils.GetLogger('messagerecall.models.domain_user')
_USER_ROWS_FETCH_PAGE = 10

USER_STARTED = 'Started'
USER_RECALLING = 'Recalling'
USER_CONNECT_FAILED = 'Imap Connect Failed'
USER_IMAP_DISABLED = 'Imap Disabled'
USER_DONE = 'Done'
USER_ABORTED = 'Aborted'
USER_SUSPENDED = 'Suspended'

USER_STATES = [USER_STARTED, USER_RECALLING, USER_CONNECT_FAILED,
               USER_IMAP_DISABLED, USER_DONE, USER_ABORTED, USER_SUSPENDED]
ACTIVE_USER_STATES = [USER_STARTED, USER_RECALLING, USER_CONNECT_FAILED,
                      USER_IMAP_DISABLED, USER_ABORTED]
TERMINAL_USER_STATES = [USER_CONNECT_FAILED, USER_IMAP_DISABLED,
"""Database models for root Message Recall Task entity."""

import time

import log_utils
from models import domain_user
from models import error_reason
import recall_errors

from google.appengine.datastore.datastore_query import Cursor
from google.appengine.ext import ndb


_GET_ENTITY_RETRIES = 5
_GET_ENTITY_SLEEP_S = 2
_LOG = log_utils.GetLogger('messagerecall.models.recall_task')
_TASK_ROWS_FETCH_PAGE = 10

TASK_STARTED = 'Started'
TASK_GETTING_USERS = 'Getting Users'
TASK_RECALLING = 'Recalling'
TASK_DONE = 'Done'

TASK_STATES = [TASK_STARTED, TASK_GETTING_USERS, TASK_RECALLING, TASK_DONE]


class RecallTaskModel(ndb.Model):
  """Model for each running/completed message recall task."""

  owner_email = ndb.StringProperty(required=True)
  message_criteria = ndb.StringProperty(required=True)
Пример #7
0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Functions to search users using the Google Admin SDK API."""

import httplib

from apiclient.discovery import build
from apiclient.errors import HttpError
import credentials_utils
import log_utils

_LOG = log_utils.GetLogger('messagerecall.user_retriever')
_MAX_RESULT_PAGE_SIZE = 500  # Default is 100.


class DomainUserRetriever(object):
    """Class to organize large, multi-page user searches.

  Uses http_utils to add error handling and retry using backoff.
  """
    def __init__(self, owner_email, user_domain, search_query):
        """Initialize the search class.

    Build the items needed to page through domain user lists which are expected
    to be >100k users at times.  Need a users collection object from the ADMIN
    SDK to reference the search api and an authenticated http connection to
    invoke it.
owned by multiple users in an apps domain.
"""

import os

import http_utils
import log_utils
from oauth2client import client
import recall_errors
import service_account

from google.appengine.api import memcache

_ACCESS_TOKEN_CACHE_S = 60 * 59  # Access tokens last about 1 hour.
_CACHE_NAMESPACE = 'messagerecall_accesstoken#ns'
_LOG = log_utils.GetLogger('messagerecall.credentials_utils')

# Load the key in PKCS 12 format that you downloaded from the Google API
# Console when you created your Service account.
_SERVICE_ACCOUNT_PEM_FILE_NAME = os.path.join(os.path.dirname(__file__),
                                              'messagerecall_privatekey.pem')

with open(_SERVICE_ACCOUNT_PEM_FILE_NAME, 'rb') as f:
    _SERVICE_ACCOUNT_KEY = f.read()


def _GetSignedJwtAssertionCredentials(user_email):
    """Retrieve an OAuth2 credentials object impersonating user_email.

  This object is then used to authorize an http connection that will be
  used to connect with Google services such as the Admin SDK.
# limitations under the License.
"""Sharded counter implementation: allows high volume counter access.

Reference: https://developers.google.com/appengine/articles/sharding_counters
"""

import random

import log_utils
import recall_errors

from google.appengine.api import memcache
from google.appengine.ext import ndb

_COUNTER_MEMCACHE_EXPIRATION_S = 60 * 60 * 24
_LOG = log_utils.GetLogger('messagerecall.models.sharded_counter')
_SHARD_KEY_TEMPLATE = 'shard-{}-{:d}'
_TRANSACTIONAL_RETRIES = 8


class CounterShardConfig(ndb.Model):
    """Allows customized shard count: highly used counters need more shards."""
    num_shards = ndb.IntegerProperty(default=20)

    @classmethod
    def AllKeys(cls, name):
        """Returns all possible keys for the counter name given the config.

    Args:
      name: The name of the counter.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Helper functions and constants for http API requests."""

import httplib2

import log_utils

_EXTENDED_SOCKET_TIMEOUT_S = 10  # Default of 5s seems too short for Admin SDK.
_LOG = log_utils.GetLogger('messagerecall.http_utils')


def GetHttpObject():
    """Helper to abstract Http connection acquisition.

  Not infrequently seeing 'HTTPException: Deadline exceeded...' error when
  running in a scaled environment with > 900 users.  Adding
  _EXTENDED_SOCKET_TIMEOUT_S seems to mostly resolve this.

  Returns:
    Http connection object.
  """
    return httplib2.Http(timeout=_EXTENDED_SOCKET_TIMEOUT_S)