Ejemplo n.º 1
0
def test_merge(config):
    """Test the basic logic of the merge() function."""
    base = Contact(name='Original Name',
                   email_address='*****@*****.**')
    remote = Contact(name='New Name',
                     email_address='*****@*****.**')
    dest = Contact(name='Original Name',
                   email_address='*****@*****.**')
    merge(base, remote, dest)
    assert dest.name == 'New Name'
    assert dest.email_address == '*****@*****.**'
Ejemplo n.º 2
0
Archivo: api.py Proyecto: jre21/inbox
 def add_contact(self, account_id, contact_info):
     """Add a new contact to the specified IMAP account. Returns the ID of
     the added contact."""
     with session_scope() as db_session:
         contact = Contact(account_id=account_id, source='local',
                           provider_name=INBOX_PROVIDER_NAME,
                           uid=uuid.uuid4())
         contact.from_cereal(contact_info)
         db_session.add(contact)
         db_session.commit()
         log.info("Added contact {0}".format(contact.id))
         return contact.id
Ejemplo n.º 3
0
def test_merge_conflict(config):
    """Test that merge() raises an error on conflict."""
    base = Contact(name='Original Name',
                   email_address='*****@*****.**')
    remote = Contact(name='New Name',
                     email_address='*****@*****.**')
    dest = Contact(name='Some Other Name',
                   email_address='*****@*****.**')
    with pytest.raises(MergeError):
        merge(base, remote, dest)

    # Check no update in case of conflict
    assert dest.name == 'Some Other Name'
    assert dest.email_address == '*****@*****.**'
Ejemplo n.º 4
0
def get_contact_objects(db_session, account_id, addresses):
    """Given a list `addresses` of (name, email) pairs, return existing
    contacts with matching email. Create and also return contact objects for
    any email without a match."""
    if addresses is None:
        return []
    contacts = []
    for addr in addresses:
        if addr is None:
            continue
        name, email = addr
        canonical_email = canonicalize_address(email)
        existing_contacts = db_session.query(Contact). \
            filter(Contact.email_address == canonical_email,
                   Contact.account_id == account_id).all()
        if not existing_contacts:
            new_contact = Contact(name=name,
                                  email_address=canonical_email,
                                  account_id=account_id,
                                  source='local',
                                  provider_name='inbox',
                                  uid=uuid.uuid4().hex)
            contacts.append(new_contact)
            db_session.add(new_contact)
        else:
            contacts.extend(existing_contacts)
    return contacts
Ejemplo n.º 5
0
 def supply_contact(self, name, email_address, deleted=False):
     self._contacts.append(
         Contact(account_id=ACCOUNT_ID,
                 uid=str(self._next_uid),
                 source='remote',
                 provider_name=self.PROVIDER_NAME,
                 name=name,
                 email_address=email_address,
                 deleted=deleted))
     self._next_uid += 1
Ejemplo n.º 6
0
Archivo: crud.py Proyecto: caitp/inbox
def create(namespace, db_session, name, email):
    contact = Contact(account_id=namespace.account_id,
                      source='local',
                      provider_name=INBOX_PROVIDER_NAME,
                      uid=uuid.uuid4().hex,
                      name=name,
                      email_address=email)
    db_session.add(contact)
    db_session.commit()
    return contact
Ejemplo n.º 7
0
class GoogleContactsProvider(object):
    """A utility class to fetch and parse Google contact data for the specified
    account using the Google Contacts API.

    Parameters
    ----------
    db_session: sqlalchemy.orm.session.Session
        Database session.

    account: ..models.tables.ImapAccount
        The user account for which to fetch contact data.

    Attributes
    ----------
    google_client: gdata.contacts.client.ContactsClient
        Google API client to do the actual data fetching.
    log: logging.Logger
        Logging handler.
    """
    PROVIDER_NAME = 'google'

    def __init__(self, account_id):
        self.account_id = account_id
        self.log = configure_logging(account_id, 'googlecontacts')

    def _get_google_client(self):
        """Return the Google API client."""
        # TODO(emfree) figure out a better strategy for refreshing OAuth
        # credentials as needed
        with session_scope() as db_session:
            try:
                account = db_session.query(ImapAccount).get(self.account_id)
                account = verify_imap_account(db_session, account)
                two_legged_oauth_token = gdata.gauth.OAuth2Token(
                    client_id=GOOGLE_OAUTH_CLIENT_ID,
                    client_secret=GOOGLE_OAUTH_CLIENT_SECRET,
                    scope=OAUTH_SCOPE,
                    user_agent=SOURCE_APP_NAME,
                    access_token=account.o_access_token,
                    refresh_token=account.o_refresh_token)
                google_client = gdata.contacts.client.ContactsClient(
                    source=SOURCE_APP_NAME)
                google_client.auth_token = two_legged_oauth_token
                return google_client
            except gdata.client.BadAuthentication:
                self.log.error('Invalid user credentials given')
                return None

    def _parse_contact_result(self, google_contact):
        """Constructs a Contact object from a Google contact entry.

        Parameters
        ----------
        google_contact: gdata.contacts.entry.ContactEntry
            The Google contact entry to parse.

        Returns
        -------
        ..models.tables.base.Contact
            A corresponding Inbox Contact instance.

        Raises
        ------
        AttributeError
           If the contact data could not be parsed correctly.
        """
        email_addresses = [
            email for email in google_contact.email if email.primary
        ]
        if email_addresses and len(email_addresses) > 1:
            self.log.error(
                "Should not have more than one email per entry! {0}".format(
                    email_addresses))
        try:
            # The id.text field of a ContactEntry object takes the form
            # 'http://www.google.com/m8/feeds/contacts/<useremail>/base/<uid>'.
            # We only want the <uid> part.
            raw_google_id = google_contact.id.text
            _, g_id = posixpath.split(raw_google_id)
            name = (google_contact.name.full_name.text if
                    (google_contact.name
                     and google_contact.name.full_name) else None)
            email_address = (email_addresses[0].address
                             if email_addresses else None)

            # The entirety of the raw contact data in XML string
            # representation.
            raw_data = google_contact.to_string()
        except AttributeError, e:
            self.log.error(
                'Something is wrong with contact: {0}'.format(google_contact))
            raise e

        deleted = google_contact.deleted is not None

        return Contact(account_id=self.account_id,
                       source='remote',
                       uid=g_id,
                       name=name,
                       provider_name=self.PROVIDER_NAME,
                       email_address=email_address,
                       deleted=deleted,
                       raw_data=raw_data)
Ejemplo n.º 8
0
def poll(account_id, provider):
    """Query a remote contacts provider for updates and persist them to the
    database.

    Parameters
    ----------
    account_id: int
        ID for the account whose contacts should be queried.
    db_session: sqlalchemy.orm.session.Session
        Database session

    provider: Interface to the remote contact data provider.
        Must have a PROVIDER_NAME attribute and implement the get_contacts()
        method.
    """
    provider_name = provider.PROVIDER_NAME
    with session_scope() as db_session:
        account = db_session.query(Account).get(account_id)

        # Contact data reflecting any local modifications since the last sync
        # with the remote provider.
        local_contacts = db_session.query(Contact).filter_by(
            source='local', account=account,
            provider_name=provider_name).all()
        # Snapshot of contact data from immediately after last sync.
        cached_contacts = db_session.query(Contact).filter_by(
            source='remote', account=account,
            provider_name=provider_name).all()
        log.info('Query: have {0} contacts, {1} cached.'.format(
            len(local_contacts), len(cached_contacts)))

        cached_contact_dict = {contact.uid: contact for contact in
                               cached_contacts}
        local_contact_dict = {contact.uid: contact for contact in
                              local_contacts}

        change_counter = Counter()
        last_sync = or_none(account.last_synced_contacts,
                            datetime.datetime.isoformat)
        to_commit = []
        for remote_contact in provider.get_contacts(last_sync):
            remote_contact.account = account
            assert remote_contact.uid is not None, \
                'Got remote contact with null uid'
            assert isinstance(remote_contact.uid, str)
            cached_contact = cached_contact_dict.get(remote_contact.uid)
            local_contact = local_contact_dict.get(remote_contact.uid)
            # If the remote contact was deleted, purge the corresponding
            # database entries.
            if remote_contact.deleted:
                if cached_contact is not None:
                    db_session.delete(cached_contact)
                    change_counter['deleted'] += 1
                if local_contact is not None:
                    db_session.delete(local_contact)
                continue
            # Otherwise, update the database.
            if cached_contact is not None:
                # The provider gave an update to a contact we already have.
                if local_contact is not None:
                    try:
                        # Attempt to merge remote updates into local_contact
                        merge(cached_contact, remote_contact, local_contact)
                        # And update cached_contact to reflect both local and
                        # remote updates
                        cached_contact.copy_from(local_contact)
                    except MergeError:
                        log.error('Conflicting local and remote updates to '
                                  'contact.\nLocal: {0}\ncached: {1}\n '
                                  'remote: {2}'.format(local_contact,
                                                       cached_contact,
                                                       remote_contact))
                        # TODO(emfree): Come up with a strategy for handling
                        # merge conflicts. For now, just don't update if there
                        # is a conflict.
                        continue
                else:
                    log.warning('Contact {0} already present as remote but '
                                'not local contact'.format(cached_contact))
                    cached_contact.copy_from(remote_contact)
                change_counter['updated'] += 1
            else:
                # This is a new contact, create both local and remote DB
                # entries.
                local_contact = Contact()
                local_contact.copy_from(remote_contact)
                local_contact.source = 'local'
                to_commit.append(local_contact)
                to_commit.append(remote_contact)
                change_counter['added'] += 1

        account.last_synced_contacts = datetime.datetime.now()

        log.info('Added {0} contacts.'.format(change_counter['added']))
        log.info('Updated {0} contacts.'.format(change_counter['updated']))
        log.info('Deleted {0} contacts.'.format(change_counter['deleted']))

        db_session.add_all(to_commit)
        db_session.commit()
Ejemplo n.º 9
0
def poll(account_id, provider):
    """Query a remote contacts provider for updates and persist them to the
    database.

    Parameters
    ----------
    account_id: int
        ID for the account whose contacts should be queried.
    db_session: sqlalchemy.orm.session.Session
        Database session

    provider: Interface to the remote contact data provider.
        Must have a PROVIDER_NAME attribute and implement the get_contacts()
        method.
    """
    provider_name = provider.PROVIDER_NAME
    with session_scope() as db_session:
        account = db_session.query(Account).get(account_id)
        change_counter = Counter()
        last_sync = or_none(account.last_synced_contacts,
                            datetime.datetime.isoformat)
        to_commit = []
        for remote_contact in provider.get_contacts(last_sync):
            remote_contact.account = account
            assert remote_contact.uid is not None, \
                'Got remote contact with null uid'
            assert isinstance(remote_contact.uid, str)
            matching_contacts = db_session.query(Contact).filter(
                Contact.account == account,
                Contact.provider_name == provider_name,
                Contact.uid == remote_contact.uid)
            # Snapshot of contact data from immediately after last sync:
            cached_contact = matching_contacts. \
                filter(Contact.source == 'remote').first()
            # Contact data reflecting any local modifications since the last
            # sync with the remote provider:
            local_contact = matching_contacts. \
                filter(Contact.source == 'local').first()
            # If the remote contact was deleted, purge the corresponding
            # database entries.
            if remote_contact.deleted:
                if cached_contact is not None:
                    db_session.delete(cached_contact)
                    change_counter['deleted'] += 1
                if local_contact is not None:
                    db_session.delete(local_contact)
                continue
            # Otherwise, update the database.
            if cached_contact is not None:
                # The provider gave an update to a contact we already have.
                if local_contact is not None:
                    try:
                        # Attempt to merge remote updates into local_contact
                        merge(cached_contact, remote_contact, local_contact)
                        # And update cached_contact to reflect both local and
                        # remote updates
                        cached_contact.copy_from(local_contact)
                    except MergeError:
                        log.error('Conflicting local and remote updates to '
                                  'contact.\nLocal: {0}\ncached: {1}\n '
                                  'remote: {2}'.format(local_contact,
                                                       cached_contact,
                                                       remote_contact))
                        # TODO(emfree): Come up with a strategy for handling
                        # merge conflicts. For now, just don't update if there
                        # is a conflict.
                        continue
                else:
                    log.warning('Contact {0} already present as remote but '
                                'not local contact'.format(cached_contact))
                    cached_contact.copy_from(remote_contact)
                change_counter['updated'] += 1
            else:
                # This is a new contact, create both local and remote DB
                # entries.
                local_contact = Contact()
                local_contact.copy_from(remote_contact)
                local_contact.source = 'local'
                to_commit.append(local_contact)
                to_commit.append(remote_contact)
                change_counter['added'] += 1

        account.last_synced_contacts = datetime.datetime.now()

        log.info('Added {0} contacts.'.format(change_counter['added']))
        log.info('Updated {0} contacts.'.format(change_counter['updated']))
        log.info('Deleted {0} contacts.'.format(change_counter['deleted']))

        db_session.add_all(to_commit)
        db_session.commit()