|  | 
 | """IMAP4 client. | 
 |  | 
 | Based on RFC 2060. | 
 |  | 
 | Author: Piers Lauder <piers@cs.su.oz.au> December 1997. | 
 |  | 
 | Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998. | 
 |  | 
 | Public class:		IMAP4 | 
 | Public variable:	Debug | 
 | Public functions:	Internaldate2tuple | 
 | 			Int2AP | 
 | 			ParseFlags | 
 | 			Time2Internaldate | 
 | """ | 
 |  | 
 | __version__ = "2.16" | 
 |  | 
 | import binascii, re, socket, string, time, random, sys | 
 |  | 
 | #	Globals | 
 |  | 
 | CRLF = '\r\n' | 
 | Debug = 0 | 
 | IMAP4_PORT = 143 | 
 | AllowedVersions = ('IMAP4REV1', 'IMAP4')	# Most recent first | 
 |  | 
 | #	Commands | 
 |  | 
 | Commands = { | 
 | 	# name		  valid states | 
 | 	'APPEND':	('AUTH', 'SELECTED'), | 
 | 	'AUTHENTICATE':	('NONAUTH',), | 
 | 	'CAPABILITY':	('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), | 
 | 	'CHECK':	('SELECTED',), | 
 | 	'CLOSE':	('SELECTED',), | 
 | 	'COPY':		('SELECTED',), | 
 | 	'CREATE':	('AUTH', 'SELECTED'), | 
 | 	'DELETE':	('AUTH', 'SELECTED'), | 
 | 	'EXAMINE':	('AUTH', 'SELECTED'), | 
 | 	'EXPUNGE':	('SELECTED',), | 
 | 	'FETCH':	('SELECTED',), | 
 | 	'LIST':		('AUTH', 'SELECTED'), | 
 | 	'LOGIN':	('NONAUTH',), | 
 | 	'LOGOUT':	('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), | 
 | 	'LSUB':		('AUTH', 'SELECTED'), | 
 | 	'NOOP':		('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), | 
 | 	'PARTIAL':	('SELECTED',), | 
 | 	'RENAME':	('AUTH', 'SELECTED'), | 
 | 	'SEARCH':	('SELECTED',), | 
 | 	'SELECT':	('AUTH', 'SELECTED'), | 
 | 	'STATUS':	('AUTH', 'SELECTED'), | 
 | 	'STORE':	('SELECTED',), | 
 | 	'SUBSCRIBE':	('AUTH', 'SELECTED'), | 
 | 	'UID':		('SELECTED',), | 
 | 	'UNSUBSCRIBE':	('AUTH', 'SELECTED'), | 
 | 	} | 
 |  | 
 | #	Patterns to match server responses | 
 |  | 
 | Continuation = re.compile(r'\+( (?P<data>.*))?') | 
 | Flags = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)') | 
 | InternalDate = re.compile(r'.*INTERNALDATE "' | 
 | 	r'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])' | 
 | 	r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])' | 
 | 	r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])' | 
 | 	r'"') | 
 | Literal = re.compile(r'(?P<data>.*) {(?P<size>\d+)}$') | 
 | Response_code = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]') | 
 | Untagged_response = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?') | 
 | Untagged_status = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?') | 
 |  | 
 |  | 
 |  | 
 | class IMAP4: | 
 |  | 
 | 	"""IMAP4 client class. | 
 |  | 
 | 	Instantiate with: IMAP4([host[, port]]) | 
 |  | 
 | 		host - host's name (default: localhost); | 
 | 		port - port number (default: standard IMAP4 port). | 
 |  | 
 | 	All IMAP4rev1 commands are supported by methods of the same | 
 | 	name (in lower-case). | 
 |  | 
 | 	All arguments to commands are converted to strings, except for | 
 | 	AUTHENTICATE, and the last argument to APPEND which is passed as | 
 | 	an IMAP4 literal.  If necessary (the string contains | 
 | 	white-space and isn't enclosed with either parentheses or | 
 | 	double quotes) each string is quoted. However, the 'password' | 
 | 	argument to the LOGIN command is always quoted. | 
 |  | 
 | 	Each command returns a tuple: (type, [data, ...]) where 'type' | 
 | 	is usually 'OK' or 'NO', and 'data' is either the text from the | 
 | 	tagged response, or untagged results from command. | 
 |  | 
 | 	Errors raise the exception class <instance>.error("<reason>"). | 
 | 	IMAP4 server errors raise <instance>.abort("<reason>"), | 
 | 	which is a sub-class of 'error'. Mailbox status changes | 
 | 	from READ-WRITE to READ-ONLY raise the exception class | 
 | 	<instance>.readonly("<reason>"), which is a sub-class of 'abort'. | 
 |  | 
 | 	"error" exceptions imply a program error. | 
 | 	"abort" exceptions imply the connection should be reset, and | 
 | 		the command re-tried. | 
 | 	"readonly" exceptions imply the command should be re-tried. | 
 |  | 
 | 	Note: to use this module, you must read the RFCs pertaining | 
 | 	to the IMAP4 protocol, as the semantics of the arguments to | 
 | 	each IMAP4 command are left to the invoker, not to mention | 
 | 	the results. | 
 | 	""" | 
 |  | 
 | 	class error(Exception): pass	# Logical errors - debug required | 
 | 	class abort(error): pass	# Service errors - close and retry | 
 | 	class readonly(abort): pass	# Mailbox status changed to READ-ONLY | 
 |  | 
 | 	mustquote = re.compile(r'\W')	# Match any non-alphanumeric character | 
 |  | 
 | 	def __init__(self, host = '', port = IMAP4_PORT): | 
 | 		self.host = host | 
 | 		self.port = port | 
 | 		self.debug = Debug | 
 | 		self.state = 'LOGOUT' | 
 | 		self.literal = None		# A literal argument to a command | 
 | 		self.tagged_commands = {}	# Tagged commands awaiting response | 
 | 		self.untagged_responses = {}	# {typ: [data, ...], ...} | 
 | 		self.continuation_response = ''	# Last continuation response | 
 | 		self.tagnum = 0 | 
 |  | 
 | 		# Open socket to server. | 
 |  | 
 | 		self.open(host, port) | 
 |  | 
 | 		# Create unique tag for this session, | 
 | 		# and compile tagged response matcher. | 
 |  | 
 | 		self.tagpre = Int2AP(random.randint(0, 31999)) | 
 | 		self.tagre = re.compile(r'(?P<tag>' | 
 | 				+ self.tagpre | 
 | 				+ r'\d+) (?P<type>[A-Z]+) (?P<data>.*)') | 
 |  | 
 | 		# Get server welcome message, | 
 | 		# request and store CAPABILITY response. | 
 |  | 
 | 		if __debug__: | 
 | 			if self.debug >= 1: | 
 | 				_mesg('new IMAP4 connection, tag=%s' % self.tagpre) | 
 |  | 
 | 		self.welcome = self._get_response() | 
 | 		if self.untagged_responses.has_key('PREAUTH'): | 
 | 			self.state = 'AUTH' | 
 | 		elif self.untagged_responses.has_key('OK'): | 
 | 			self.state = 'NONAUTH' | 
 | 		else: | 
 | 			raise self.error(self.welcome) | 
 |  | 
 | 		cap = 'CAPABILITY' | 
 | 		self._simple_command(cap) | 
 | 		if not self.untagged_responses.has_key(cap): | 
 | 			raise self.error('no CAPABILITY response from server') | 
 | 		self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1]))) | 
 |  | 
 | 		if __debug__: | 
 | 			if self.debug >= 3: | 
 | 				_mesg('CAPABILITIES: %s' % `self.capabilities`) | 
 |  | 
 | 		for version in AllowedVersions: | 
 | 			if not version in self.capabilities: | 
 | 				continue | 
 | 			self.PROTOCOL_VERSION = version | 
 | 			return | 
 |  | 
 | 		raise self.error('server not IMAP4 compliant') | 
 |  | 
 |  | 
 | 	def __getattr__(self, attr): | 
 | 		#	Allow UPPERCASE variants of IMAP4 command methods. | 
 | 		if Commands.has_key(attr): | 
 | 			return eval("self.%s" % string.lower(attr)) | 
 | 		raise AttributeError("Unknown IMAP4 command: '%s'" % attr) | 
 |  | 
 |  | 
 |  | 
 | 	#	Public methods | 
 |  | 
 |  | 
 | 	def open(self, host, port): | 
 | 		"""Setup 'self.sock' and 'self.file'.""" | 
 | 		self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | 
 | 		self.sock.connect(self.host, self.port) | 
 | 		self.file = self.sock.makefile('r') | 
 |  | 
 |  | 
 | 	def recent(self): | 
 | 		"""Return most recent 'RECENT' responses if any exist, | 
 | 		else prompt server for an update using the 'NOOP' command. | 
 |  | 
 | 		(typ, [data]) = <instance>.recent() | 
 |  | 
 | 		'data' is None if no new messages, | 
 | 		else list of RECENT responses, most recent last. | 
 | 		""" | 
 | 		name = 'RECENT' | 
 | 		typ, dat = self._untagged_response('OK', [None], name) | 
 | 		if dat[-1]: | 
 | 			return typ, dat | 
 | 		typ, dat = self.noop()	# Prod server for response | 
 | 		return self._untagged_response(typ, dat, name) | 
 |  | 
 |  | 
 | 	def response(self, code): | 
 | 		"""Return data for response 'code' if received, or None. | 
 |  | 
 | 		Old value for response 'code' is cleared. | 
 |  | 
 | 		(code, [data]) = <instance>.response(code) | 
 | 		""" | 
 | 		return self._untagged_response(code, [None], string.upper(code)) | 
 |  | 
 |  | 
 | 	def socket(self): | 
 | 		"""Return socket instance used to connect to IMAP4 server. | 
 |  | 
 | 		socket = <instance>.socket() | 
 | 		""" | 
 | 		return self.sock | 
 |  | 
 |  | 
 |  | 
 | 	#	IMAP4 commands | 
 |  | 
 |  | 
 | 	def append(self, mailbox, flags, date_time, message): | 
 | 		"""Append message to named mailbox. | 
 |  | 
 | 		(typ, [data]) = <instance>.append(mailbox, flags, date_time, message) | 
 |  | 
 | 			All args except `message' can be None. | 
 | 		""" | 
 | 		name = 'APPEND' | 
 | 		if not mailbox: | 
 | 			mailbox = 'INBOX' | 
 | 		if flags: | 
 | 			if (flags[0],flags[-1]) != ('(',')'): | 
 | 				flags = '(%s)' % flags | 
 | 		else: | 
 | 			flags = None | 
 | 		if date_time: | 
 | 			date_time = Time2Internaldate(date_time) | 
 | 		else: | 
 | 			date_time = None | 
 | 		self.literal = message | 
 | 		return self._simple_command(name, mailbox, flags, date_time) | 
 |  | 
 |  | 
 | 	def authenticate(self, mechanism, authobject): | 
 | 		"""Authenticate command - requires response processing. | 
 |  | 
 | 		'mechanism' specifies which authentication mechanism is to | 
 | 		be used - it must appear in <instance>.capabilities in the | 
 | 		form AUTH=<mechanism>. | 
 |  | 
 | 		'authobject' must be a callable object: | 
 |  | 
 | 			data = authobject(response) | 
 |  | 
 | 		It will be called to process server continuation responses. | 
 | 		It should return data that will be encoded and sent to server. | 
 | 		It should return None if the client abort response '*' should | 
 | 		be sent instead. | 
 | 		""" | 
 | 		mech = string.upper(mechanism) | 
 | 		cap = 'AUTH=%s' % mech | 
 | 		if not cap in self.capabilities: | 
 | 			raise self.error("Server doesn't allow %s authentication." % mech) | 
 | 		self.literal = _Authenticator(authobject).process | 
 | 		typ, dat = self._simple_command('AUTHENTICATE', mech) | 
 | 		if typ != 'OK': | 
 | 			raise self.error(dat[-1]) | 
 | 		self.state = 'AUTH' | 
 | 		return typ, dat | 
 |  | 
 |  | 
 | 	def check(self): | 
 | 		"""Checkpoint mailbox on server. | 
 |  | 
 | 		(typ, [data]) = <instance>.check() | 
 | 		""" | 
 | 		return self._simple_command('CHECK') | 
 |  | 
 |  | 
 | 	def close(self): | 
 | 		"""Close currently selected mailbox. | 
 |  | 
 | 		Deleted messages are removed from writable mailbox. | 
 | 		This is the recommended command before 'LOGOUT'. | 
 |  | 
 | 		(typ, [data]) = <instance>.close() | 
 | 		""" | 
 | 		try: | 
 | 			typ, dat = self._simple_command('CLOSE') | 
 | 		finally: | 
 | 			self.state = 'AUTH' | 
 | 		return typ, dat | 
 |  | 
 |  | 
 | 	def copy(self, message_set, new_mailbox): | 
 | 		"""Copy 'message_set' messages onto end of 'new_mailbox'. | 
 |  | 
 | 		(typ, [data]) = <instance>.copy(message_set, new_mailbox) | 
 | 		""" | 
 | 		return self._simple_command('COPY', message_set, new_mailbox) | 
 |  | 
 |  | 
 | 	def create(self, mailbox): | 
 | 		"""Create new mailbox. | 
 |  | 
 | 		(typ, [data]) = <instance>.create(mailbox) | 
 | 		""" | 
 | 		return self._simple_command('CREATE', mailbox) | 
 |  | 
 |  | 
 | 	def delete(self, mailbox): | 
 | 		"""Delete old mailbox. | 
 |  | 
 | 		(typ, [data]) = <instance>.delete(mailbox) | 
 | 		""" | 
 | 		return self._simple_command('DELETE', mailbox) | 
 |  | 
 |  | 
 | 	def expunge(self): | 
 | 		"""Permanently remove deleted items from selected mailbox. | 
 |  | 
 | 		Generates 'EXPUNGE' response for each deleted message. | 
 |  | 
 | 		(typ, [data]) = <instance>.expunge() | 
 |  | 
 | 		'data' is list of 'EXPUNGE'd message numbers in order received. | 
 | 		""" | 
 | 		name = 'EXPUNGE' | 
 | 		typ, dat = self._simple_command(name) | 
 | 		return self._untagged_response(typ, dat, name) | 
 |  | 
 |  | 
 | 	def fetch(self, message_set, message_parts): | 
 | 		"""Fetch (parts of) messages. | 
 |  | 
 | 		(typ, [data, ...]) = <instance>.fetch(message_set, message_parts) | 
 |  | 
 | 		'data' are tuples of message part envelope and data. | 
 | 		""" | 
 | 		name = 'FETCH' | 
 | 		typ, dat = self._simple_command(name, message_set, message_parts) | 
 | 		return self._untagged_response(typ, dat, name) | 
 |  | 
 |  | 
 | 	def list(self, directory='""', pattern='*'): | 
 | 		"""List mailbox names in directory matching pattern. | 
 |  | 
 | 		(typ, [data]) = <instance>.list(directory='""', pattern='*') | 
 |  | 
 | 		'data' is list of LIST responses. | 
 | 		""" | 
 | 		name = 'LIST' | 
 | 		typ, dat = self._simple_command(name, directory, pattern) | 
 | 		return self._untagged_response(typ, dat, name) | 
 |  | 
 |  | 
 | 	def login(self, user, password): | 
 | 		"""Identify client using plaintext password. | 
 |  | 
 | 		(typ, [data]) = <instance>.login(user, password) | 
 |  | 
 | 		NB: 'password' will be quoted. | 
 | 		""" | 
 | 		#if not 'AUTH=LOGIN' in self.capabilities: | 
 | 		#	raise self.error("Server doesn't allow LOGIN authentication." % mech) | 
 | 		typ, dat = self._simple_command('LOGIN', user, self._quote(password)) | 
 | 		if typ != 'OK': | 
 | 			raise self.error(dat[-1]) | 
 | 		self.state = 'AUTH' | 
 | 		return typ, dat | 
 |  | 
 |  | 
 | 	def logout(self): | 
 | 		"""Shutdown connection to server. | 
 |  | 
 | 		(typ, [data]) = <instance>.logout() | 
 |  | 
 | 		Returns server 'BYE' response. | 
 | 		""" | 
 | 		self.state = 'LOGOUT' | 
 | 		try: typ, dat = self._simple_command('LOGOUT') | 
 | 		except: typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]] | 
 | 		self.file.close() | 
 | 		self.sock.close() | 
 | 		if self.untagged_responses.has_key('BYE'): | 
 | 			return 'BYE', self.untagged_responses['BYE'] | 
 | 		return typ, dat | 
 |  | 
 |  | 
 | 	def lsub(self, directory='""', pattern='*'): | 
 | 		"""List 'subscribed' mailbox names in directory matching pattern. | 
 |  | 
 | 		(typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*') | 
 |  | 
 | 		'data' are tuples of message part envelope and data. | 
 | 		""" | 
 | 		name = 'LSUB' | 
 | 		typ, dat = self._simple_command(name, directory, pattern) | 
 | 		return self._untagged_response(typ, dat, name) | 
 |  | 
 |  | 
 | 	def noop(self): | 
 | 		"""Send NOOP command. | 
 |  | 
 | 		(typ, data) = <instance>.noop() | 
 | 		""" | 
 | 		if __debug__: | 
 | 			if self.debug >= 3: | 
 | 				_dump_ur(self.untagged_responses) | 
 | 		return self._simple_command('NOOP') | 
 |  | 
 |  | 
 | 	def partial(self, message_num, message_part, start, length): | 
 | 		"""Fetch truncated part of a message. | 
 |  | 
 | 		(typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length) | 
 |  | 
 | 		'data' is tuple of message part envelope and data. | 
 | 		""" | 
 | 		name = 'PARTIAL' | 
 | 		typ, dat = self._simple_command(name, message_num, message_part, start, length) | 
 | 		return self._untagged_response(typ, dat, 'FETCH') | 
 |  | 
 |  | 
 | 	def rename(self, oldmailbox, newmailbox): | 
 | 		"""Rename old mailbox name to new. | 
 |  | 
 | 		(typ, data) = <instance>.rename(oldmailbox, newmailbox) | 
 | 		""" | 
 | 		return self._simple_command('RENAME', oldmailbox, newmailbox) | 
 |  | 
 |  | 
 | 	def search(self, charset, criteria): | 
 | 		"""Search mailbox for matching messages. | 
 |  | 
 | 		(typ, [data]) = <instance>.search(charset, criteria) | 
 |  | 
 | 		'data' is space separated list of matching message numbers. | 
 | 		""" | 
 | 		name = 'SEARCH' | 
 | 		if charset: | 
 | 			charset = 'CHARSET ' + charset | 
 | 		typ, dat = self._simple_command(name, charset, criteria) | 
 | 		return self._untagged_response(typ, dat, name) | 
 |  | 
 |  | 
 | 	def select(self, mailbox='INBOX', readonly=None): | 
 | 		"""Select a mailbox. | 
 |  | 
 | 		Flush all untagged responses. | 
 |  | 
 | 		(typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None) | 
 |  | 
 | 		'data' is count of messages in mailbox ('EXISTS' response). | 
 | 		""" | 
 | 		# Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY') | 
 | 		self.untagged_responses = {}	# Flush old responses. | 
 | 		if readonly: | 
 | 			name = 'EXAMINE' | 
 | 		else: | 
 | 			name = 'SELECT' | 
 | 		typ, dat = self._simple_command(name, mailbox) | 
 | 		if typ != 'OK': | 
 | 			self.state = 'AUTH'	# Might have been 'SELECTED' | 
 | 			return typ, dat | 
 | 		self.state = 'SELECTED' | 
 | 		if not self.untagged_responses.has_key('READ-WRITE') \ | 
 | 			and not readonly: | 
 | 			if __debug__: | 
 | 				if self.debug >= 1: | 
 | 					_dump_ur(self.untagged_responses) | 
 | 			raise self.readonly('%s is not writable' % mailbox) | 
 | 		return typ, self.untagged_responses.get('EXISTS', [None]) | 
 |  | 
 |  | 
 | 	def status(self, mailbox, names): | 
 | 		"""Request named status conditions for mailbox. | 
 |  | 
 | 		(typ, [data]) = <instance>.status(mailbox, names) | 
 | 		""" | 
 | 		name = 'STATUS' | 
 | 		if self.PROTOCOL_VERSION == 'IMAP4': | 
 | 			raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name) | 
 | 		typ, dat = self._simple_command(name, mailbox, names) | 
 | 		return self._untagged_response(typ, dat, name) | 
 |  | 
 |  | 
 | 	def store(self, message_set, command, flag_list): | 
 | 		"""Alters flag dispositions for messages in mailbox. | 
 |  | 
 | 		(typ, [data]) = <instance>.store(message_set, command, flag_list) | 
 | 		""" | 
 | 		typ, dat = self._simple_command('STORE', message_set, command, flag_list) | 
 | 		return self._untagged_response(typ, dat, 'FETCH') | 
 |  | 
 |  | 
 | 	def subscribe(self, mailbox): | 
 | 		"""Subscribe to new mailbox. | 
 |  | 
 | 		(typ, [data]) = <instance>.subscribe(mailbox) | 
 | 		""" | 
 | 		return self._simple_command('SUBSCRIBE', mailbox) | 
 |  | 
 |  | 
 | 	def uid(self, command, *args): | 
 | 		"""Execute "command arg ..." with messages identified by UID, | 
 | 			rather than message number. | 
 |  | 
 | 		(typ, [data]) = <instance>.uid(command, arg1, arg2, ...) | 
 |  | 
 | 		Returns response appropriate to 'command'. | 
 | 		""" | 
 | 		command = string.upper(command) | 
 | 		if not Commands.has_key(command): | 
 | 			raise self.error("Unknown IMAP4 UID command: %s" % command) | 
 | 		if self.state not in Commands[command]: | 
 | 			raise self.error('command %s illegal in state %s' | 
 | 						% (command, self.state)) | 
 | 		name = 'UID' | 
 | 		typ, dat = apply(self._simple_command, (name, command) + args) | 
 | 		if command == 'SEARCH': | 
 | 			name = 'SEARCH' | 
 | 		else: | 
 | 			name = 'FETCH' | 
 | 		return self._untagged_response(typ, dat, name) | 
 |  | 
 |  | 
 | 	def unsubscribe(self, mailbox): | 
 | 		"""Unsubscribe from old mailbox. | 
 |  | 
 | 		(typ, [data]) = <instance>.unsubscribe(mailbox) | 
 | 		""" | 
 | 		return self._simple_command('UNSUBSCRIBE', mailbox) | 
 |  | 
 |  | 
 | 	def xatom(self, name, *args): | 
 | 		"""Allow simple extension commands | 
 | 			notified by server in CAPABILITY response. | 
 |  | 
 | 		(typ, [data]) = <instance>.xatom(name, arg, ...) | 
 | 		""" | 
 | 		if name[0] != 'X' or not name in self.capabilities: | 
 | 			raise self.error('unknown extension command: %s' % name) | 
 | 		return apply(self._simple_command, (name,) + args) | 
 |  | 
 |  | 
 |  | 
 | 	#	Private methods | 
 |  | 
 |  | 
 | 	def _append_untagged(self, typ, dat): | 
 |  | 
 | 		if dat is None: dat = '' | 
 | 		ur = self.untagged_responses | 
 | 		if __debug__: | 
 | 			if self.debug >= 5: | 
 | 				_mesg('untagged_responses[%s] %s += ["%s"]' % | 
 | 					(typ, len(ur.get(typ,'')), dat)) | 
 | 		if ur.has_key(typ): | 
 | 			ur[typ].append(dat) | 
 | 		else: | 
 | 			ur[typ] = [dat] | 
 |  | 
 |  | 
 | 	def _check_bye(self): | 
 | 		bye = self.untagged_responses.get('BYE') | 
 | 		if bye: | 
 | 			raise self.abort(bye[-1]) | 
 |  | 
 |  | 
 | 	def _command(self, name, *args): | 
 |  | 
 | 		if self.state not in Commands[name]: | 
 | 			self.literal = None | 
 | 			raise self.error( | 
 | 			'command %s illegal in state %s' % (name, self.state)) | 
 |  | 
 | 		for typ in ('OK', 'NO', 'BAD'): | 
 | 			if self.untagged_responses.has_key(typ): | 
 | 				del self.untagged_responses[typ] | 
 |  | 
 | 		if self.untagged_responses.has_key('READ-WRITE') \ | 
 | 		and self.untagged_responses.has_key('READ-ONLY'): | 
 | 			del self.untagged_responses['READ-WRITE'] | 
 | 			raise self.readonly('mailbox status changed to READ-ONLY') | 
 |  | 
 | 		tag = self._new_tag() | 
 | 		data = '%s %s' % (tag, name) | 
 | 		for arg in args: | 
 | 			if arg is None: continue | 
 | 			data = '%s %s' % (data, self._checkquote(arg)) | 
 |  | 
 | 		literal = self.literal | 
 | 		if literal is not None: | 
 | 			self.literal = None | 
 | 			if type(literal) is type(self._command): | 
 | 				literator = literal | 
 | 			else: | 
 | 				literator = None | 
 | 				data = '%s {%s}' % (data, len(literal)) | 
 |  | 
 | 		if __debug__: | 
 | 			if self.debug >= 4: | 
 | 				_mesg('> %s' % data) | 
 | 			else: | 
 | 				_log('> %s' % data) | 
 |  | 
 | 		try: | 
 | 			self.sock.send('%s%s' % (data, CRLF)) | 
 | 		except socket.error, val: | 
 | 			raise self.abort('socket error: %s' % val) | 
 |  | 
 | 		if literal is None: | 
 | 			return tag | 
 |  | 
 | 		while 1: | 
 | 			# Wait for continuation response | 
 |  | 
 | 			while self._get_response(): | 
 | 				if self.tagged_commands[tag]:	# BAD/NO? | 
 | 					return tag | 
 |  | 
 | 			# Send literal | 
 |  | 
 | 			if literator: | 
 | 				literal = literator(self.continuation_response) | 
 |  | 
 | 			if __debug__: | 
 | 				if self.debug >= 4: | 
 | 					_mesg('write literal size %s' % len(literal)) | 
 |  | 
 | 			try: | 
 | 				self.sock.send(literal) | 
 | 				self.sock.send(CRLF) | 
 | 			except socket.error, val: | 
 | 				raise self.abort('socket error: %s' % val) | 
 |  | 
 | 			if not literator: | 
 | 				break | 
 |  | 
 | 		return tag | 
 |  | 
 |  | 
 | 	def _command_complete(self, name, tag): | 
 | 		self._check_bye() | 
 | 		try: | 
 | 			typ, data = self._get_tagged_response(tag) | 
 | 		except self.abort, val: | 
 | 			raise self.abort('command: %s => %s' % (name, val)) | 
 | 		except self.error, val: | 
 | 			raise self.error('command: %s => %s' % (name, val)) | 
 | 		self._check_bye() | 
 | 		if typ == 'BAD': | 
 | 			raise self.error('%s command error: %s %s' % (name, typ, data)) | 
 | 		return typ, data | 
 |  | 
 |  | 
 | 	def _get_response(self): | 
 |  | 
 | 		# Read response and store. | 
 | 		# | 
 | 		# Returns None for continuation responses, | 
 | 		# otherwise first response line received. | 
 |  | 
 | 		resp = self._get_line() | 
 |  | 
 | 		# Command completion response? | 
 |  | 
 | 		if self._match(self.tagre, resp): | 
 | 			tag = self.mo.group('tag') | 
 | 			if not self.tagged_commands.has_key(tag): | 
 | 				raise self.abort('unexpected tagged response: %s' % resp) | 
 |  | 
 | 			typ = self.mo.group('type') | 
 | 			dat = self.mo.group('data') | 
 | 			self.tagged_commands[tag] = (typ, [dat]) | 
 | 		else: | 
 | 			dat2 = None | 
 |  | 
 | 			# '*' (untagged) responses? | 
 |  | 
 | 			if not self._match(Untagged_response, resp): | 
 | 				if self._match(Untagged_status, resp): | 
 | 					dat2 = self.mo.group('data2') | 
 |  | 
 | 			if self.mo is None: | 
 | 				# Only other possibility is '+' (continuation) rsponse... | 
 |  | 
 | 				if self._match(Continuation, resp): | 
 | 					self.continuation_response = self.mo.group('data') | 
 | 					return None	# NB: indicates continuation | 
 |  | 
 | 				raise self.abort("unexpected response: '%s'" % resp) | 
 |  | 
 | 			typ = self.mo.group('type') | 
 | 			dat = self.mo.group('data') | 
 | 			if dat is None: dat = ''	# Null untagged response | 
 | 			if dat2: dat = dat + ' ' + dat2 | 
 |  | 
 | 			# Is there a literal to come? | 
 |  | 
 | 			while self._match(Literal, dat): | 
 |  | 
 | 				# Read literal direct from connection. | 
 |  | 
 | 				size = string.atoi(self.mo.group('size')) | 
 | 				if __debug__: | 
 | 					if self.debug >= 4: | 
 | 						_mesg('read literal size %s' % size) | 
 | 				data = self.file.read(size) | 
 |  | 
 | 				# Store response with literal as tuple | 
 |  | 
 | 				self._append_untagged(typ, (dat, data)) | 
 |  | 
 | 				# Read trailer - possibly containing another literal | 
 |  | 
 | 				dat = self._get_line() | 
 |  | 
 | 			self._append_untagged(typ, dat) | 
 |  | 
 | 		# Bracketed response information? | 
 |  | 
 | 		if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat): | 
 | 			self._append_untagged(self.mo.group('type'), self.mo.group('data')) | 
 |  | 
 | 		if __debug__: | 
 | 			if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'): | 
 | 				_mesg('%s response: %s' % (typ, dat)) | 
 |  | 
 | 		return resp | 
 |  | 
 |  | 
 | 	def _get_tagged_response(self, tag): | 
 |  | 
 | 		while 1: | 
 | 			result = self.tagged_commands[tag] | 
 | 			if result is not None: | 
 | 				del self.tagged_commands[tag] | 
 | 				return result | 
 | 			self._get_response() | 
 |  | 
 |  | 
 | 	def _get_line(self): | 
 |  | 
 | 		line = self.file.readline() | 
 | 		if not line: | 
 | 			raise self.abort('socket error: EOF') | 
 |  | 
 | 		# Protocol mandates all lines terminated by CRLF | 
 |  | 
 | 		line = line[:-2] | 
 | 		if __debug__: | 
 | 			if self.debug >= 4: | 
 | 				_mesg('< %s' % line) | 
 | 			else: | 
 | 				_log('< %s' % line) | 
 | 		return line | 
 |  | 
 |  | 
 | 	def _match(self, cre, s): | 
 |  | 
 | 		# Run compiled regular expression match method on 's'. | 
 | 		# Save result, return success. | 
 |  | 
 | 		self.mo = cre.match(s) | 
 | 		if __debug__: | 
 | 			if self.mo is not None and self.debug >= 5: | 
 | 				_mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`)) | 
 | 		return self.mo is not None | 
 |  | 
 |  | 
 | 	def _new_tag(self): | 
 |  | 
 | 		tag = '%s%s' % (self.tagpre, self.tagnum) | 
 | 		self.tagnum = self.tagnum + 1 | 
 | 		self.tagged_commands[tag] = None | 
 | 		return tag | 
 |  | 
 |  | 
 | 	def _checkquote(self, arg): | 
 |  | 
 | 		# Must quote command args if non-alphanumeric chars present, | 
 | 		# and not already quoted. | 
 |  | 
 | 		if type(arg) is not type(''): | 
 | 			return arg | 
 | 		if (arg[0],arg[-1]) in (('(',')'),('"','"')): | 
 | 			return arg | 
 | 		if self.mustquote.search(arg) is None: | 
 | 			return arg | 
 | 		return self._quote(arg) | 
 |  | 
 |  | 
 | 	def _quote(self, arg): | 
 |  | 
 | 		arg = string.replace(arg, '\\', '\\\\') | 
 | 		arg = string.replace(arg, '"', '\\"') | 
 |  | 
 | 		return '"%s"' % arg | 
 |  | 
 |  | 
 | 	def _simple_command(self, name, *args): | 
 |  | 
 | 		return self._command_complete(name, apply(self._command, (name,) + args)) | 
 |  | 
 |  | 
 | 	def _untagged_response(self, typ, dat, name): | 
 |  | 
 | 		if typ == 'NO': | 
 | 			return typ, dat | 
 | 		if not self.untagged_responses.has_key(name): | 
 | 			return typ, [None] | 
 | 		data = self.untagged_responses[name] | 
 | 		if __debug__: | 
 | 			if self.debug >= 5: | 
 | 				_mesg('untagged_responses[%s] => %s' % (name, data)) | 
 | 		del self.untagged_responses[name] | 
 | 		return typ, data | 
 |  | 
 |  | 
 |  | 
 | class _Authenticator: | 
 |  | 
 | 	"""Private class to provide en/decoding | 
 | 		for base64-based authentication conversation. | 
 | 	""" | 
 |  | 
 | 	def __init__(self, mechinst): | 
 | 		self.mech = mechinst	# Callable object to provide/process data | 
 |  | 
 | 	def process(self, data): | 
 | 		ret = self.mech(self.decode(data)) | 
 | 		if ret is None: | 
 | 			return '*'	# Abort conversation | 
 | 		return self.encode(ret) | 
 |  | 
 | 	def encode(self, inp): | 
 | 		# | 
 | 		#  Invoke binascii.b2a_base64 iteratively with | 
 | 		#  short even length buffers, strip the trailing | 
 | 		#  line feed from the result and append.  "Even" | 
 | 		#  means a number that factors to both 6 and 8, | 
 | 		#  so when it gets to the end of the 8-bit input | 
 | 		#  there's no partial 6-bit output. | 
 | 		# | 
 | 		oup = '' | 
 | 		while inp: | 
 | 			if len(inp) > 48: | 
 | 				t = inp[:48] | 
 | 				inp = inp[48:] | 
 | 			else: | 
 | 				t = inp | 
 | 				inp = '' | 
 | 			e = binascii.b2a_base64(t) | 
 | 			if e: | 
 | 				oup = oup + e[:-1] | 
 | 		return oup | 
 |    | 
 | 	def decode(self, inp): | 
 | 		if not inp: | 
 | 			return '' | 
 | 		return binascii.a2b_base64(inp) | 
 |   | 
 |  | 
 |  | 
 | Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6, | 
 | 	'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12} | 
 |  | 
 | def Internaldate2tuple(resp): | 
 |  | 
 | 	"""Convert IMAP4 INTERNALDATE to UT. | 
 |  | 
 | 	Returns Python time module tuple. | 
 | 	""" | 
 |  | 
 | 	mo = InternalDate.match(resp) | 
 | 	if not mo: | 
 | 		return None | 
 |  | 
 | 	mon = Mon2num[mo.group('mon')] | 
 | 	zonen = mo.group('zonen') | 
 |  | 
 | 	for name in ('day', 'year', 'hour', 'min', 'sec', 'zoneh', 'zonem'): | 
 | 		exec "%s = string.atoi(mo.group('%s'))" % (name, name) | 
 |  | 
 | 	# INTERNALDATE timezone must be subtracted to get UT | 
 |  | 
 | 	zone = (zoneh*60 + zonem)*60 | 
 | 	if zonen == '-': | 
 | 		zone = -zone | 
 |  | 
 | 	tt = (year, mon, day, hour, min, sec, -1, -1, -1) | 
 |  | 
 | 	utc = time.mktime(tt) | 
 |  | 
 | 	# Following is necessary because the time module has no 'mkgmtime'. | 
 | 	# 'mktime' assumes arg in local timezone, so adds timezone/altzone. | 
 |  | 
 | 	lt = time.localtime(utc) | 
 | 	if time.daylight and lt[-1]: | 
 | 		zone = zone + time.altzone | 
 | 	else: | 
 | 		zone = zone + time.timezone | 
 |  | 
 | 	return time.localtime(utc - zone) | 
 |  | 
 |  | 
 |  | 
 | def Int2AP(num): | 
 |  | 
 | 	"""Convert integer to A-P string representation.""" | 
 |  | 
 | 	val = ''; AP = 'ABCDEFGHIJKLMNOP' | 
 | 	num = int(abs(num)) | 
 | 	while num: | 
 | 		num, mod = divmod(num, 16) | 
 | 		val = AP[mod] + val | 
 | 	return val | 
 |  | 
 |  | 
 |  | 
 | def ParseFlags(resp): | 
 |  | 
 | 	"""Convert IMAP4 flags response to python tuple.""" | 
 |  | 
 | 	mo = Flags.match(resp) | 
 | 	if not mo: | 
 | 		return () | 
 |  | 
 | 	return tuple(string.split(mo.group('flags'))) | 
 |  | 
 |  | 
 | def Time2Internaldate(date_time): | 
 |  | 
 | 	"""Convert 'date_time' to IMAP4 INTERNALDATE representation. | 
 |  | 
 | 	Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"' | 
 | 	""" | 
 |  | 
 | 	dttype = type(date_time) | 
 | 	if dttype is type(1) or dttype is type(1.1): | 
 | 		tt = time.localtime(date_time) | 
 | 	elif dttype is type(()): | 
 | 		tt = date_time | 
 | 	elif dttype is type(""): | 
 | 		return date_time	# Assume in correct format | 
 | 	else: raise ValueError | 
 |  | 
 | 	dt = time.strftime("%d-%b-%Y %H:%M:%S", tt) | 
 | 	if dt[0] == '0': | 
 | 		dt = ' ' + dt[1:] | 
 | 	if time.daylight and tt[-1]: | 
 | 		zone = -time.altzone | 
 | 	else: | 
 | 		zone = -time.timezone | 
 | 	return '"' + dt + " %+02d%02d" % divmod(zone/60, 60) + '"' | 
 |  | 
 |  | 
 |  | 
 | if __debug__: | 
 |  | 
 | 	def _mesg(s, secs=None): | 
 | 		if secs is None: | 
 | 			secs = time.time() | 
 | 		tm = time.strftime('%M:%S', time.localtime(secs)) | 
 | 		sys.stderr.write('  %s.%02d %s\n' % (tm, (secs*100)%100, s)) | 
 | 		sys.stderr.flush() | 
 |  | 
 | 	def _dump_ur(dict): | 
 | 		# Dump untagged responses (in `dict'). | 
 | 		l = dict.items() | 
 | 		if not l: return | 
 | 		t = '\n\t\t' | 
 | 		j = string.join | 
 | 		l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l) | 
 | 		_mesg('untagged responses dump:%s%s' % (t, j(l, t))) | 
 |  | 
 | 	_cmd_log = []		# Last `_cmd_log_len' interactions | 
 | 	_cmd_log_len = 10 | 
 |  | 
 | 	def _log(line): | 
 | 		# Keep log of last `_cmd_log_len' interactions for debugging. | 
 | 		if len(_cmd_log) == _cmd_log_len: | 
 | 			del _cmd_log[0] | 
 | 		_cmd_log.append((time.time(), line)) | 
 |  | 
 | 	def print_log(): | 
 | 		_mesg('last %d IMAP4 interactions:' % len(_cmd_log)) | 
 | 		for secs,line in _cmd_log: | 
 | 			_mesg(line, secs) | 
 |  | 
 |  | 
 |  | 
 | if __name__ == '__main__': | 
 |  | 
 | 	import getpass, sys | 
 |  | 
 | 	host = '' | 
 | 	if sys.argv[1:]: host = sys.argv[1] | 
 |  | 
 | 	USER = getpass.getuser() | 
 | 	PASSWD = getpass.getpass("IMAP password for %s: " % (host or "localhost")) | 
 |  | 
 | 	test_seq1 = ( | 
 | 	('login', (USER, PASSWD)), | 
 | 	('create', ('/tmp/xxx 1',)), | 
 | 	('rename', ('/tmp/xxx 1', '/tmp/yyy')), | 
 | 	('CREATE', ('/tmp/yyz 2',)), | 
 | 	('append', ('/tmp/yyz 2', None, None, 'From: anon@x.y.z\n\ndata...')), | 
 | 	('list', ('/tmp', 'yy*')), | 
 | 	('select', ('/tmp/yyz 2',)), | 
 | 	('search', (None, '(TO zork)')), | 
 | 	('partial', ('1', 'RFC822', 1, 1024)), | 
 | 	('store', ('1', 'FLAGS', '(\Deleted)')), | 
 | 	('expunge', ()), | 
 | 	('recent', ()), | 
 | 	('close', ()), | 
 | 	) | 
 |  | 
 | 	test_seq2 = ( | 
 | 	('select', ()), | 
 | 	('response',('UIDVALIDITY',)), | 
 | 	('uid', ('SEARCH', 'ALL')), | 
 | 	('response', ('EXISTS',)), | 
 | 	('append', (None, None, None, 'From: anon@x.y.z\n\ndata...')), | 
 | 	('recent', ()), | 
 | 	('logout', ()), | 
 | 	) | 
 |  | 
 | 	def run(cmd, args): | 
 | 		_mesg('%s %s' % (cmd, args)) | 
 | 		typ, dat = apply(eval('M.%s' % cmd), args) | 
 | 		_mesg('%s => %s %s' % (cmd, typ, dat)) | 
 | 		return dat | 
 |  | 
 | 	Debug = 5 | 
 | 	M = IMAP4(host) | 
 | 	_mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION) | 
 |  | 
 | 	for cmd,args in test_seq1: | 
 | 		run(cmd, args) | 
 |  | 
 | 	for ml in run('list', ('/tmp/', 'yy%')): | 
 | 		mo = re.match(r'.*"([^"]+)"$', ml) | 
 | 		if mo: path = mo.group(1) | 
 | 		else: path = string.split(ml)[-1] | 
 | 		run('delete', (path,)) | 
 |  | 
 | 	for cmd,args in test_seq2: | 
 | 		dat = run(cmd, args) | 
 |  | 
 | 		if (cmd,args) != ('uid', ('SEARCH', 'ALL')): | 
 | 			continue | 
 |  | 
 | 		uid = string.split(dat[-1]) | 
 | 		if not uid: continue | 
 | 		run('uid', ('FETCH', '%s' % uid[-1], | 
 | 			'(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)')) |