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_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 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))
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)
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)
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))
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 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 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))
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)
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))
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 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)
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)
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 _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) + ")")
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))
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)
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)
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 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))
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 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))
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))
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))
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_connection(connection): if is_auth_error(connection): return _cmd(callback, connection) else: return _cmd_cb(connection.create, _on_mailbox_creation, bool(callback), name)
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)
def _on_close(imap_response): return _cmd_cb(self.conn.logout, _on_logout, bool(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))