Example #1
0
    def connection(self, callback=None):
        """Creates an authenticated connection to gmail over IMAP

        Attempts to authenticate a connection with the gmail server using
        xoauth if a connection string has been provided, and otherwise using
        the provided password.

        If a connection has already successfully been created, no action will
        be taken (so multiplie calls to this method will result in a single
        connection effort, once a connection has been successfully created).

        Returns:
            pygmail.account.AuthError, if the given connection parameters are
            not accepted by the Gmail server, and otherwise an imaplib2
            connection object.

        """
        def _on_ids(connection):
            _cmd(callback, connection)

        def _on_authentication(imap_response):
            is_error = check_for_response_error(imap_response)

            if is_error:
                if self.oauth2_token:
                    error = "User / OAuth2 token (%s, %s) were not accepted" % (
                        self.email, self.oauth2_token)
                else:
                    error = "User / password (%s, %s) were not accepted" % (
                        self.email, self.password)
                return _cmd(callback, AuthError(error))
            else:
                self.connected = True
                if self.id_params:
                    return _cmd_cb(self.id, _on_ids, bool(callback))
                else:
                    return _cmd(callback, self.conn)

        if self.connected:
            return _cmd(callback, self.conn)
        elif self.oauth2_token:
            auth_params = self.email, self.oauth2_token
            xoauth2_string = 'user=%s\1auth=Bearer %s\1\1' % auth_params
            try:
                return _cmd_cb(self.conn.authenticate,
                              _on_authentication, bool(callback),
                              "XOAUTH2", lambda x: xoauth2_string)
            except:
                return _cmd(callback, AuthError(""))
        else:
            try:
                return _cmd_cb(self.conn.login, _on_authentication,
                              bool(callback), self.email, self.password)
            except Exception, e:
                return _cmd(callback, e)
Example #2
0
 def _on_trash_selected(imap_response, force_success=False):
     # It can take several attempts for the deleted message to show up
     # in the trash label / folder.  We'll try 5 times, waiting
     # two sec between each attempt
     if force_success:
         return _cmd_cb(self.conn, _on_received_connection_3, bool(callback))
     else:
         is_error = check_for_response_error(imap_response)
         if is_error:
             return _cmd(callback, is_error)
         else:
             return _cmd_cb(self.conn, _on_received_connection_3, bool(callback))
Example #3
0
    def add_mailbox(self, name, callback=None):
        """Creates a new mailbox / folder in the current account. This is
        implemented using the gmail X-GM-LABELS IMAP extension.

        Args:
            name -- the name of the folder to create in the gmail account.

        Return:
            True if a new folder / label was created. Otherwise, False (such
            as if the folder already exists)
        """

        def _on_mailbox_creation(imap_response):
            error = check_for_response_error(imap_response)
            if error:
                return _cmd(callback, error)
            else:
                response_type = extract_type(imap_response)
                if response_type == "NO":
                    return _cmd(callback, False)
                else:
                    data = extract_data(imap_response)
                    self.boxes = None
                    was_success = data[0] == "Success"
                    return _cmd(callback, was_success)

        @pygmail.errors.check_imap_state(callback)
        def _on_connection(connection):
            if is_auth_error(connection):
                return _cmd(callback, connection)
            else:
                return _cmd_cb(connection.create, _on_mailbox_creation,
                               bool(callback), name)

        return _cmd_cb(self.connection, _on_connection, bool(callback))
Example #4
0
 def _on_safe_save_connection(connection, message_copy):
     cbp = dict(message_copy=message_copy)
     return _cmd_cb(connection.append, _on_safe_save_append,
                    bool(callback),  self.mailbox.name, '(\Seen)',
                    self.internal_date or time.gmtime(),
                    message_copy.as_string(),
                    callback_args=cbp)
Example #5
0
    def full_message(self, callback=None):
        """Fetches the full version of the message that this message is a teaser
        version of."""
        def _on_full_message_fetched(full_msg):
            return _cmd(callback, full_msg)

        return _cmd_cb(self.mailbox.fetch, _on_full_message_fetched, bool(callback), self.uid, full=True)
Example #6
0
    def id(self, callback=None):
        """Sends the ID command to the Gmail server, as requested / suggested
        by [Google](https://developers.google.com/google-apps/gmail/imap_extensions)

        The order that the terms will be sent in undefined, but each key
        will come immediatly before its value.

        Args:
            params -- A dictionary of terms that should be sent to google.

        Returns:
            The imaplib2 connection object on success, and an error object
            otherwise
        """
        @pygmail.errors.check_imap_response(callback)
        def _on_id(imap_response):
            return _cmd(callback, self.conn)

        @pygmail.errors.check_imap_state(callback)
        def _on_connection(connection):
            id_params = []
            for k, v in self.id_params.items():
                id_params.append('"' + k + '"')
                id_params.append('"' + v + '"')
            # The IMAPlib2 exposed version of the "ID" command doesn't
            # format the parameters the same way gmail wants them, so
            # we just do it ourselves (imaplib2 wraps them in an extra
            # paren)
            return _cmd_cb(connection._simple_command, _on_id,
                           bool(callback), 'ID',
                           "(" + " ".join(id_params) + ")")

        return _cmd_cb(self.connection, _on_connection, bool(callback))
Example #7
0
    def mailboxes(self, callback=None):
        """Returns a list of all mailboxes in the current account

        Keyword Args:
            callback     -- optional callback function, which will cause the
                            conection to operate in an async mode
        Returns:
            A list of pygmail.mailbox.Mailbox objects, each representing one
            mailbox in the IMAP account

        """
        if self.boxes is not None:
            return _cmd(callback, self.boxes)
        else:
            @pygmail.errors.check_imap_response(callback)
            def _on_mailboxes(imap_response):
                data = extract_data(imap_response)
                self.boxes = []
                for box in data:
                    self.boxes.append(mailbox.Mailbox(self, box))
                return _cmd(callback, self.boxes)

            @pygmail.errors.check_imap_state(callback)
            def _on_connection(connection):
                if is_auth_error(connection) or is_imap_error(connection):
                    return _cmd(callback, connection)
                else:
                    return _cmd_cb(connection.list, _on_mailboxes, bool(callback))

            return _cmd_cb(self.connection, _on_connection, bool(callback))
Example #8
0
    def close(self, callback=None):
        """Closes the IMAP connection to GMail

        Closes and logs out of the IMAP connection to GMail.

        Returns:
            True if a connection was closed, and False if this close request
            was a NOOP
        """
        @pygmail.errors.check_imap_response(callback, require_ok=False)
        def _on_logout(imap_response):
            typ = extract_type(imap_response)
            self.connected = False
            return _cmd(callback, typ == "BYE")

        @pygmail.errors.check_imap_response(callback, require_ok=False)
        def _on_close(imap_response):
            return _cmd_cb(self.conn.logout, _on_logout, bool(callback))

        if self.last_viewed_mailbox:
            try:
                return _cmd_cb(self.conn.close, _on_close, bool(callback))
            except Exception as e:
                return _cmd(callback, IMAPError(e))
        else:
            return _on_close(None)
Example #9
0
    def get(self, mailbox_name, callback=None):
        """Returns the mailbox with a given name in the current account

        Args:
            mailbox_name -- The name of a mailbox to look for in the current
                            account

        Keyword Args:
            callback -- optional callback function, which will cause the
                        conection to operate in an async mode

        Returns:
            None if no mailbox matching the given name could be found.
            Otherwise, returns the pygmail.mailbox.Mailbox object representing
            the mailbox.

        """
        @pygmail.errors.check_imap_response(callback)
        def _retreived_mailboxes(mailboxes):
            for mailbox in mailboxes:
                if mailbox.name == mailbox_name:
                    return _cmd(callback, mailbox)
            return _cmd(callback, None)

        return _cmd_cb(self.mailboxes, _retreived_mailboxes, bool(callback))
Example #10
0
 def _on_search(imap_response):
     data = extract_data(imap_response)
     ids = string.split(data[0])
     ids_to_fetch = page_from_list(ids, limit, offset)
     return _cmd_cb(self.messages_by_id, _on_messages_by_id,
                    bool(callback), ids_to_fetch, only_uids=only_uids,
                    full=full, teaser=teasers, gm_ids=gm_ids)
Example #11
0
    def messages(self, limit=100, offset=0, callback=None, **kwargs):
        """Returns a list of all the messages in the inbox

        Fetches a list of all messages in the inbox.  This list is by default
        limited to only the first 100 results, though pagination can trivially
        be implemented using the limit / offset parameters

        Keyword arguments:
            limit     -- The maximum number of messages to return.  If None,
                         everything will be returned
            offset    -- The first message to return out of the entire set of
                         messages in the inbox
            gm_ids    -- If True, only the unique, persistant X-GM-MSGID
                         value for the email message will be returned
            only_uids -- If True, only the UIDs of the matching messages will
                         be returned, instead of full message headers.
            full      -- Whether to fetch the entire message, instead of
                         just the headers.  Note that if only_uids is True,
                         this parameter will have no effect.
            teaser    -- Whether to fetch just a brief, teaser version of the
                         body (ie the first mime section).  Note that this
                         option is incompatible with the full
                         option, and the former will take precedence

        Return:

            A two index tupple.  The element in the first index is a
            list of zero or more pygmail.message.Message objects (or uids if
            only_uids is TRUE), or None if no information could be found about
            the mailbox. The second element is the total number of messages (not
            just those returned from the limit-offset parameters)

        """
        teasers = kwargs.get('teaser')
        full = kwargs.get('full')
        only_uids = kwargs.get('only_uids')
        gm_ids = kwargs.get('gm_ids')

        def _on_messages_by_id(messages):
            return _cmd(callback, messages)

        @pygmail.errors.check_imap_response(callback)
        def _on_search(imap_response):
            data = extract_data(imap_response)
            ids = string.split(data[0])
            ids_to_fetch = page_from_list(ids, limit, offset)
            return _cmd_cb(self.messages_by_id, _on_messages_by_id,
                           bool(callback), ids_to_fetch, only_uids=only_uids,
                           full=full, teaser=teasers, gm_ids=gm_ids)

        @pygmail.errors.check_imap_state(callback)
        def _on_connection(connection):
            return _cmd_cb(connection.search, _on_search, bool(callback), None, 'ALL')

        @pygmail.errors.check_imap_response(callback)
        def _on_select_complete(result):
            return _cmd_cb(self.account.connection, _on_connection, bool(callback))

        return _cmd_cb(self.select, _on_select_complete, bool(callback))
Example #12
0
 def _on_search_complete(imap_response):
     data = extract_data(imap_response)
     if len(data) == 0 or not data[0]:
         return _cmd(callback, None)
     else:
         uid = data[0]
         return _cmd_cb(self.fetch, _on_fetch, bool(callback),
                        uid, full=full, **kwargs)
Example #13
0
    def teaser(self, callback=None):
        """Fetches an abbreviated, teaser version of the message, containing
        just the text of the first text or html part of the message body
        """
        def _on_teaser_fetched(teaser):
            return _cmd(callback, teaser)

        return _cmd_cb(self.mailbox.fetch, _on_teaser_fetched, bool(callback), self.uid, teaser=True)
Example #14
0
 def _on_connection(connection):
     if gm_ids:
         request = imap_queries["gm_id"]
     elif full:
         request = imap_queries["body"]
     elif teasers:
         request = imap_queries["teaser"]
     else:
         request = imap_queries["header"]
     return _cmd_cb(connection.uid, _on_fetch, bool(callback),
                    "FETCH", uid, request)
Example #15
0
    def fetch_all(self, uids, full=False, callback=None, **kwargs):
        """Returns a list of messages, each specified by their UID

        Returns zero or more GmailMessage objects, each representing a email
        message in the current mailbox.

        Arguments:
            uids -- A list of zero or more email uids

        Keyword Args:
            gm_ids  -- If True, only the unique, persistant X-GM-MSGID
                       value for the email message will be returned
            full    -- Whether to fetch the entire message, instead of
                       just the headers.  Note that if only_uids is True,
                       this parameter will have no effect.
            teaser  -- Whether to fetch just a brief, teaser version of the
                       body (ie the first mime section).  Note that this
                       option is incompatible with the full
                       option, and the former will take precedence

        Returns:
            Zero or more pygmail.message.Message objects, representing any
            messages that matched a provided uid
        """
        teasers = kwargs.get("teaser")
        gm_ids = kwargs.get('gm_ids')

        @pygmail.errors.check_imap_response(callback)
        def _on_fetch(imap_response):
            data = extract_data(imap_response)
            messages = parse_fetch_request(data, self, teasers, full, gm_ids)
            return _cmd(callback, messages)

        @pygmail.errors.check_imap_state(callback)
        def _on_connection(connection):
            if gm_ids:
                request = imap_queries["gm_id"]
            elif full:
                request = imap_queries["body"]
            elif teasers:
                request = imap_queries["teaser"]
            else:
                request = imap_queries["header"]
            return _cmd_cb(connection.uid, _on_fetch, bool(callback),
                           "FETCH", ",".join(uids), request)

        def _on_select(result):
            return _cmd_cb(self.account.connection, _on_connection, bool(callback))

        if uids:
            return _cmd_cb(self.select, _on_select, bool(callback))
        else:
            return _cmd(callback, None)
Example #16
0
 def _on_connection(connection):
     id_params = []
     for k, v in self.id_params.items():
         id_params.append('"' + k + '"')
         id_params.append('"' + v + '"')
     # The IMAPlib2 exposed version of the "ID" command doesn't
     # format the parameters the same way gmail wants them, so
     # we just do it ourselves (imaplib2 wraps them in an extra
     # paren)
     return _cmd_cb(connection._simple_command, _on_id,
                    bool(callback), 'ID',
                    "(" + " ".join(id_params) + ")")
Example #17
0
    def fetch(self, uid, full=False, callback=None, **kwargs):
        """Returns a single message from the mailbox by UID

        Returns a single message object, representing the message in the current
        mailbox with the specific UID

        Arguments:
            uid -- the numeric, unique identifier of the message in the mailbox

        Keyword Args:
            gm_ids  -- If True, only the unique, persistant X-GM-MSGID
                       value for the email message will be returned
            full    -- Whether to fetch the entire message, instead of
                       just the headers.  Note that if only_uids is True,
                       this parameter will have no effect.
            teaser  -- Whether to fetch just a brief, teaser version of the
                       body (ie the first mime section).  Note that this
                       option is incompatible with the full
                       option, and the former will take precedence

        Returns:
            A pygmail.message.Message object representing the email message, or
            None if none could be found.  If an error is encountered, an
            IMAPError object will be returned.
        """
        teasers = kwargs.get("teaser")
        gm_ids = kwargs.get('gm_ids')

        @pygmail.errors.check_imap_response(callback)
        def _on_fetch(imap_response):
            data = extract_data(imap_response)
            messages = parse_fetch_request(data, self, teasers, full, gm_ids)
            return _cmd(callback, messages[0] if len(messages) > 0 else None)

        @pygmail.errors.check_imap_state(callback)
        def _on_connection(connection):
            if gm_ids:
                request = imap_queries["gm_id"]
            elif full:
                request = imap_queries["body"]
            elif teasers:
                request = imap_queries["teaser"]
            else:
                request = imap_queries["header"]
            return _cmd_cb(connection.uid, _on_fetch, bool(callback),
                           "FETCH", uid, request)

        @pygmail.errors.check_imap_response(callback)
        def _on_select(result):
            return _cmd_cb(self.account.connection, _on_connection, bool(callback))

        return _cmd_cb(self.select, _on_select, bool(callback))
Example #18
0
 def _on_connection(connection):
     if gm_ids:
         request = imap_queries["gm_id"]
     elif only_uids:
         request = imap_queries["uid"]
     elif full:
         request = imap_queries["body"]
     elif teasers:
         request = imap_queries["teaser"]
     else:
         request = imap_queries["header"]
     return _cmd_cb(connection.fetch, _on_fetch, bool(callback),
                    ",".join(ids), request)
Example #19
0
 def _on_post_append_connection(connection):
     # Since the X-GM-LABELS are formmated in a very non ovious way
     # using ATOM, STRING, and ASTRING formatting, each with different
     # types of escaping, we don't bother trying to parse it, at least
     # for the time being.  We just send the raw value sent to use
     # from gmail back at them.
     #
     # This has the substantial downside though that there is no
     # nice / easy way to add / remove labels from pygmail messages,
     # at least currently
     #
     # @todo parse and rewrite labels correctly
     labels_value = '(%s)' % (self.labels_raw or '',)
     return _cmd_cb(connection.uid, _on_post_labeling, bool(callback),
                    "STORE", self.uid, "+X-GM-LABELS", labels_value)
Example #20
0
        def _on_authentication(imap_response):
            is_error = check_for_response_error(imap_response)

            if is_error:
                if self.oauth2_token:
                    error = "User / OAuth2 token (%s, %s) were not accepted" % (
                        self.email, self.oauth2_token)
                else:
                    error = "User / password (%s, %s) were not accepted" % (
                        self.email, self.password)
                return _cmd(callback, AuthError(error))
            else:
                self.connected = True
                if self.id_params:
                    return _cmd_cb(self.id, _on_ids, bool(callback))
                else:
                    return _cmd(callback, self.conn)
Example #21
0
    def fetch_gm_id(self, gm_id, full=False, callback=None, **kwargs):
        """Fetches a single message from the mailbox, specified by the
        given X-GM-MSGID.

        Arguments:
            gm_id -- a numeric, globally unique identifier for a gmail message

        Keyword Args:
            full         -- Whether to fetch the entire message, instead of
                            just the headers.  Note that if only_uids is True,
                            this parameter will have no effect.
            teaser       -- Whether to fetch just a brief, teaser version of the
                            body (ie the first mime section).  Note that this
                            option is incompatible with the full
                            option, and the former will take precedence

        Returns:
            A pygmail.message.Message object representing the email message, or
            None if none could be found.  If an error is encountered, an
            IMAPError object will be returned.
        """
        def _on_fetch(message):
            return _cmd(callback, message)

        @pygmail.errors.check_imap_response(callback)
        def _on_search_complete(imap_response):
            data = extract_data(imap_response)
            if len(data) == 0 or not data[0]:
                return _cmd(callback, None)
            else:
                uid = data[0]
                return _cmd_cb(self.fetch, _on_fetch, bool(callback),
                               uid, full=full, **kwargs)

        @pygmail.errors.check_imap_state(callback)
        def _on_connection(connection):
            return _cmd_cb(connection.uid, _on_search_complete, bool(callback),
                           'search', None, 'X-GM-MSGID', gm_id)

        @pygmail.errors.check_imap_response(callback)
        def _on_select(result):
            return _cmd_cb(self.account.connection, _on_connection, bool(callback))

        return _cmd_cb(self.select, _on_select, bool(callback))
Example #22
0
    def select(self, callback=None):
        """Sets this mailbox as the current active one on the IMAP connection

        In order to make sure we don't make many many redundant calls to the
        IMAP server, we allow the account managing object to keep track
        of which mailbox was last set as active.  If the current mailbox is
        active, this method does nothing.

        Returns:
            True if any changes were made, otherwise False

        """
        def _on_count_complete(num):
            self.account.last_viewed_mailbox = self
            return _cmd(callback, True)

        if self is self.account.last_viewed_mailbox:
            return _cmd(callback, False)
        else:
            return _cmd_cb(self.count, _on_count_complete, bool(callback))
Example #23
0
    def trash_mailbox(self, callback=None):
        """Returns a mailbox object that represents the [Gmail]/Trash folder
        in the current account.  Note that this will reutrn the correct folder,
        regardless of the langauge of the current account

        Returns:
            A pygmail.mailbox.Mailbox instance representing the current,
            localized version of the [Gmail]/Trash folder, or None
            if there was an error and one couldn't be found
        """
        @pygmail.errors.check_imap_response(callback)
        def _on_mailboxes(mailboxes):
            for box in mailboxes:
                if box.full_name.find('(\HasNoChildren \Trash)') == 0:
                    return _cmd(callback, box)
            return _cmd(callback, None)

        if self.boxes:
            return _on_mailboxes(self.boxes)
        else:
            return _cmd_cb(self.mailboxes, _on_mailboxes, bool(callback))
Example #24
0
    def count(self, callback=None):
        """Returns a count of the number of emails in the mailbox

        Returns:
            The int value of the number of emails in the mailbox, or None on
            error

        """
        @pygmail.errors.check_imap_response(callback)
        def _on_select_complete(imap_response):
            data = extract_data(imap_response)
            self.account.last_viewed_mailbox = self
            msg_count = int(Mailbox.COUNT_PATTERN.sub("", str(data)))
            return _cmd(callback, msg_count)

        @pygmail.errors.check_imap_state(callback)
        def _on_connection(connection):
            return _cmd_cb(connection.select, _on_select_complete,
                           bool(callback), self.name)

        return _cmd_cb(self.account.connection, _on_connection, bool(callback))
Example #25
0
    def delete(self, callback=None):
        """Removes the mailbox / folder from the current gmail account. In
        Gmail's implementation, this translates into deleting a Gmail label.

        Return:
            True if a folder / label was removed. Otherwise, False (such
            as if the current folder / label doesn't exist at deletion)
        """
        @pygmail.errors.check_imap_response(callback, require_ok=False)
        def _on_mailbox_deletion(imap_response):
            data = extract_data(imap_response)
            was_success = data[0] == "Success"
            return _cmd(callback, was_success)

        @pygmail.errors.check_imap_state(callback)
        def _on_connection(connection):
            if pygmail.errors.is_auth_error(connection):
                return _cmd(callback, connection)
            else:
                return _cmd_cb(connection.delete, _on_mailbox_deletion,
                               bool(callback), self.name)

        return _cmd_cb(self.account.connection, _on_connection, bool(callback))
Example #26
0
        def _on_search_for_message_complete(imap_response):
            data = extract_data(imap_response)

            # Its possible here that we've tried to select the message
            # we want to delete from the trash bin before google has
            # registered it there for us.  If our search attempt returned
            # a uid, then we're good to go and can continue.
            try:
                deleted_uid = data[0].split()[-1]
                cbp = dict(deleted_uid=deleted_uid)
                return _cmd_cb(self.conn, _on_received_connection_4,
                               bool(callback), callback_args=cbp)

            # If not though, we should wait a couple of seconds and try
            # again.  We'll do this a maximum of 5 times.  If we still
            # haven't had any luck at this point, we give up and return
            # False, indiciating we weren't able to delete the message
            # fully.
            except IndexError:
                self.num_tries += 1

                # If this is the 5th time we're trying to delete this
                # message, we're going to call it a loss and stop trying.
                # We do some minimal clean up and then just bail out
                # Otherwise, schedule another attempt in 2 seconds and
                # hope that gmail has updated its indexes by then
                if self.num_tries == 5:
                    del self.num_tries
                    if __debug__:
                        _log.error(u"Giving up trying to delete message {subject} - {id}".format(subject=self.subject, id=self.message_id))
                        _log.error("got response: {response}".format(response=str(imap_response)))
                    return _cmd(callback, False)
                else:
                    if __debug__:
                        _log.error("Try {num} to delete deleting message {subject} - {id} failed.  Waiting".format(num=self.num_tries, subject=self.subject, id=self.message_id))
                        _log.error("got response: {response}".format(response=str(imap_response)))
                    return _cmd_in(_on_trash_selected, 2, bool(callback), force_success=True)
Example #27
0
 def _on_connection(connection):
     if is_auth_error(connection):
         return _cmd(callback, connection)
     else:
         return _cmd_cb(connection.create, _on_mailbox_creation,
                        bool(callback), name)
Example #28
0
 def _on_safe_save_message(message_copy):
     cbp = dict(message_copy=message_copy)
     return _cmd_cb(self.conn, _on_safe_save_connection, bool(callback),
                    callback_args=cbp)
Example #29
0
 def _on_close(imap_response):
     return _cmd_cb(self.conn.logout, _on_logout, bool(callback))
Example #30
0
 def _on_connection(connection):
     if is_auth_error(connection) or is_imap_error(connection):
         return _cmd(callback, connection)
     else:
         return _cmd_cb(connection.list, _on_mailboxes, bool(callback))