예제 #1
0
 def _on_fetch(imap_response):
     data = extract_data(imap_response)
     if only_uids:
         uids = [string.split(elm, " ")[4][:-1] for elm in data]
         return _cmd(callback, uids)
     else:
         messages = parse_fetch_request(data, self, teasers, full, gm_ids)
         return _cmd(callback, messages)
예제 #2
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)
예제 #3
0
 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)
예제 #4
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))
예제 #5
0
    def attachments(self, callback=None):
        """Returns a list of attachment portions of the current message.

        Returns:
            A list of zero or more pygmail.message.Attachment objects
        """
        # First try returning a cached version of all the attachments
        # associated with this message.  If one doesn't exist, we need to fetch
        # and build the associated attribute objects

        try:
            return _cmd(callback, self._attachments)
        except AttributeError:
            is_attachment = lambda x: x['Content-Disposition'] and "attachment" in x['Content-Disposition']
            self._attachments = [Attachment(s, self) for s in self.raw.walk() if is_attachment(s)]
            return _cmd(callback, self._attachments)
예제 #6
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)
예제 #7
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)
예제 #8
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)
예제 #9
0
 def inner(*args, **kwargs):
     imap_response = args[0]
     if isinstance(imap_response, tuple):
         error = check_for_response_error(imap_response, require_ok=require_ok)
         if error:
             if callback:
                 return _cmd(callback, error)
             else:
                 return error
         else:
             return func(*args, **kwargs)
     elif is_imap_error(imap_response) or is_auth_error(imap_response) or is_connection_closed_error(imap_response):
         if callback:
             return _cmd(callback, imap_response)
         else:
             return imap_response
     else:
         return func(*args, **kwargs)
예제 #10
0
 def inner(*args, **kwargs):
     conn = args[0]
     if not isinstance(conn, imaplib.IMAP4) or conn.state == imaplib.imaplib.LOGOUT:
         rs = IMAPClosedError('IMAP in state LOGOUT', func.__name__)
         if callback:
             return _cmd(callback, rs)
         else:
             return rs
     else:
         return func(*args, **kwargs)
예제 #11
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))
예제 #12
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)
예제 #13
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))
예제 #14
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)
예제 #15
0
 def _on_save(was_success):
     return _cmd(callback, was_success)
예제 #16
0
 def _on_ids(connection):
     _cmd(callback, connection)
예제 #17
0
 def _retreived_mailboxes(mailboxes):
     for mailbox in mailboxes:
         if mailbox.name == mailbox_name:
             return _cmd(callback, mailbox)
     return _cmd(callback, None)
예제 #18
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))
예제 #19
0
 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)
예제 #20
0
 def _on_mailboxes(mailboxes):
     for box in mailboxes:
         if box.full_name.find('(\HasNoChildren \Trash)') == 0:
             return _cmd(callback, box)
     return _cmd(callback, None)
예제 #21
0
 def _on_mailboxes(mailboxes):
     for box in mailboxes:
         box_fn = box.full_name
         if box_fn.find('(\HasNoChildren \All)') == 0 or box_fn.find('(\All \HasNoChildren)') == 0:
             return _cmd(callback, box)
     return _cmd(callback, None)
예제 #22
0
 def _on_teaser_fetched(teaser):
     return _cmd(callback, teaser)
예제 #23
0
 def _on_logout(imap_response):
     typ = extract_type(imap_response)
     self.connected = False
     return _cmd(callback, typ == "BYE")
예제 #24
0
    def replace(self, find, replace, trash_folder, callback=None):
        """Performs a body-wide string search and replace

        Note that this search-and-replace is pretty dumb, and will fail
        in, for example, HTML messages where HTML tags would alter the search
        string.

        Args:
            find         -- the search term to look for as a string, or a tuple
                            of items to replace with corresponding items in the
                            replace tuple
            replace      -- the string to replace instances of the "find" term
                            with, or a tuple of terms to replace the
                            corresponding strings in the find tuple
            trash_folder -- the name of the folder / label that is, in the
                            current account, the trash container

        Returns:
            True on success, and in all other instances an error object
        """
        def _set_content_transfer_encoding(part, encoding):
            try:
                del part['Content-Transfer-Encoding']
            except:
                ""
            part.add_header('Content-Transfer-Encoding', encoding)

        valid_content_types = ('plain', 'html')

        for valid_type in valid_content_types:

            for part in typed_subpart_iterator(self.raw, 'text', valid_type):

                section_encoding = part['Content-Transfer-Encoding']

                # If the message section doesn't advertise an encoding,
                # then default to quoted printable. Otherwise the module
                # will default to base64, which can cause problems
                if not section_encoding:
                    section_encoding = "quoted-printable"
                else:
                    section_encoding = section_encoding.lower()

                section_charset = message_part_charset(part, self.raw)
                new_payload_section = utf8_encode_message_part(
                    part, self.raw, section_charset)

                if is_encoding_error(new_payload_section):
                    self.encoding_error = new_payload_section
                    return _cmd(callback, self.encoding_error)

                if isinstance(find, tuple) or isinstance(find, list):
                    for i in range(0, len(find)):
                        new_payload_section = new_payload_section.replace(
                            find[i], replace[i])
                else:
                    new_payload_section = new_payload_section.replace(
                        find, replace)

                new_payload_section = new_payload_section.encode(
                    part._orig_charset, errors="replace")

                if section_encoding == "quoted-printable":
                    new_payload_section = encodestring(new_payload_section,
                                                       quotetabs=0)
                    part.set_payload(new_payload_section, part._orig_charset)
                    _set_content_transfer_encoding(part, "quoted-printable")
                elif section_encoding == "base64":
                    part.set_payload(new_payload_section, part._orig_charset)
                    ENC.encode_base64(part)
                    _set_content_transfer_encoding(part, "base64")
                elif section_encoding in ('7bit', '8bit'):
                    part.set_payload(new_payload_section, part._orig_charset)
                    ENC.encode_7or8bit(part)
                    _set_content_transfer_encoding(part, section_encoding)
                elif section_encoding == "binary":
                    part.set_payload(new_payload_section, part._orig_charset)
                    part['Content-Transfer-Encoding'] = 'binary'
                    _set_content_transfer_encoding(part, 'binary')

                del part._normalized
                del part._orig_charset

        def _on_save(was_success):
            return _cmd(callback, was_success)

        return _cmd_cb(self.save, _on_save, bool(callback), trash_folder)
예제 #25
0
 def _on_post_labeling(imap_response):
     return _cmd(callback, True)
예제 #26
0
 def _on_full_msg_fetched(full_msg):
     return _cmd(callback, full_msg)
예제 #27
0
 def _on_id(imap_response):
     return _cmd(callback, self.conn)
예제 #28
0
 def _on_post_safe_labeling(imap_response, message_uid, message_id):
     response = (message_uid, message_id) if message_uid else False
     return _cmd(callback, response)
예제 #29
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)
예제 #30
0
    def safe_save_message(self, header_label="PyGmail", callback=None):
        """Create a text version of the message that is similar to, but not
        identical to, the current message. The text version of this is intented
        to be different enough that it can safely live in the gmail account
        alongside the current version of the message.

        Keyword Args:
            header_label -- The label to use when writing serialized state into
                            the header of this message

        Returns:
            A message object, which is a near copy of the current message,
            but with a new message id and with the flags and labels serilized
            into a header.
        """
        from base64 import b64encode
        try:
            import cPickle as pickle
        except:
            import pickle

        copied_message = email.message_from_string(self.raw.as_string())

        stripped_headers = []
        # First seralize the state we'll loose when we write this copy
        # of the message to a safe, second location

        for header_to_copy in ('In-Reply-To', 'References', 'Sender'):
            try:
                header_value = copied_message[header_to_copy]
                del copied_message[header_to_copy]
                stripped_headers.append((header_to_copy, header_value))
            except:
                pass

        serialized_data = dict(message_id=self.message_id, flags=self.flags,
                               labels=self.labels_raw, headers=stripped_headers,
                               subject=copied_message['Subject'])

        serilization = pickle.dumps(serialized_data)
        custom_header = "X-%s-Data" % (header_label,)
        copied_message[custom_header] = b64encode(serilization)

        h = sha1()
        h.update(copied_message[custom_header])

        # Next generate a new unique ID we can use for identifying this
        # message. The only requirement here is to be unique in the account
        # and to be formatted correctly.
        new_message_id = "<%s@pygmail>" % (h.hexdigest(),)

        try:
            copied_message.replace_header("Message-Id", new_message_id)
        except:
            copied_message.add_header("Message-Id", new_message_id)
        new_subject = " ** %s - Backup ** " % (copied_message['Subject'],)
        try:
            copied_message.replace_header('Subject', new_subject)
        except KeyError:
            copied_message.add_header('Subject', new_subject)
        return _cmd(callback, copied_message)