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)
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)
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)
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))
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)
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)
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)
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)
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)
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)
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))
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)
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))
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)
def _on_save(was_success): return _cmd(callback, was_success)
def _on_ids(connection): _cmd(callback, connection)
def _retreived_mailboxes(mailboxes): for mailbox in mailboxes: if mailbox.name == mailbox_name: return _cmd(callback, mailbox) return _cmd(callback, None)
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))
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)
def _on_mailboxes(mailboxes): for box in mailboxes: if box.full_name.find('(\HasNoChildren \Trash)') == 0: return _cmd(callback, box) return _cmd(callback, None)
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)
def _on_teaser_fetched(teaser): return _cmd(callback, teaser)
def _on_logout(imap_response): typ = extract_type(imap_response) self.connected = False return _cmd(callback, typ == "BYE")
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)
def _on_post_labeling(imap_response): return _cmd(callback, True)
def _on_full_msg_fetched(full_msg): return _cmd(callback, full_msg)
def _on_id(imap_response): return _cmd(callback, self.conn)
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)
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)
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)