[Python-checkins] CVS: python/dist/src/Lib imaplib.py,1.13,1.14

Guido van Rossum guido@cnri.reston.va.us
Mon, 13 Dec 1999 18:27:49 -0500 (EST)


Update of /projects/cvsroot/python/dist/src/Lib
In directory eric:/projects/python/develop/guido/src/Lib

Modified Files:
	imaplib.py 
Log Message:
V 2.16 from Piers:

	I've changed the login command to force proper
	quoting of the password argument. I've also added
	some extra debugging code, which is removed when
	__debug__ is false.


Index: imaplib.py
===================================================================
RCS file: /projects/cvsroot/python/dist/src/Lib/imaplib.py,v
retrieving revision 1.13
retrieving revision 1.14
diff -C2 -r1.13 -r1.14
*** imaplib.py	1998/10/21 22:06:56	1.13
--- imaplib.py	1999/12/13 23:27:45	1.14
***************
*** 16,20 ****
  """
  
! __version__ = "2.15"
  
  import binascii, re, socket, string, time, random, sys
--- 16,20 ----
  """
  
! __version__ = "2.16"
  
  import binascii, re, socket, string, time, random, sys
***************
*** 90,94 ****
  	an IMAP4 literal.  If necessary (the string contains
  	white-space and isn't enclosed with either parentheses or
! 	double quotes) each string is quoted.
  
  	Each command returns a tuple: (type, [data, ...]) where 'type'
--- 90,95 ----
  	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'
***************
*** 102,105 ****
--- 103,111 ----
  	<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
***************
*** 112,115 ****
--- 118,122 ----
  	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):
***************
*** 139,144 ****
  		# request and store CAPABILITY response.
  
! 		if __debug__ and self.debug >= 1:
! 			_mesg('new IMAP4 connection, tag=%s' % self.tagpre)
  
  		self.welcome = self._get_response()
--- 146,152 ----
  		# request and store CAPABILITY response.
  
! 		if __debug__:
! 			if self.debug >= 1:
! 				_mesg('new IMAP4 connection, tag=%s' % self.tagpre)
  
  		self.welcome = self._get_response()
***************
*** 147,151 ****
  		elif self.untagged_responses.has_key('OK'):
  			self.state = 'NONAUTH'
- #		elif self.untagged_responses.has_key('BYE'):
  		else:
  			raise self.error(self.welcome)
--- 155,158 ----
***************
*** 157,162 ****
  		self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1])))
  
! 		if __debug__ and self.debug >= 3:
! 			_mesg('CAPABILITIES: %s' % `self.capabilities`)
  
  		for version in AllowedVersions:
--- 164,170 ----
  		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:
***************
*** 230,235 ****
--- 238,247 ----
  
  		(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]) != ('(',')'):
***************
*** 360,367 ****
  	def login(self, user, password):
  		"""Identify client using plaintext password.
  
! 		(typ, [data]) = <instance>.list(user, password)
  		"""
! 		typ, dat = self._simple_command('LOGIN', user, password)
  		if typ != 'OK':
  			raise self.error(dat[-1])
--- 372,383 ----
  	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])
***************
*** 404,409 ****
  		(typ, data) = <instance>.noop()
  		"""
! 		if __debug__ and self.debug >= 3:
! 			_dump_ur(self.untagged_responses)
  		return self._simple_command('NOOP')
  
--- 420,426 ----
  		(typ, data) = <instance>.noop()
  		"""
! 		if __debug__:
! 			if self.debug >= 3:
! 				_dump_ur(self.untagged_responses)
  		return self._simple_command('NOOP')
  
***************
*** 465,469 ****
  		if not self.untagged_responses.has_key('READ-WRITE') \
  			and not readonly:
! 			if __debug__ and self.debug >= 1: _dump_ur(self.untagged_responses)
  			raise self.readonly('%s is not writable' % mailbox)
  		return typ, self.untagged_responses.get('EXISTS', [None])
--- 482,488 ----
  		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])
***************
*** 547,554 ****
  	def _append_untagged(self, typ, dat):
  
  		ur = self.untagged_responses
! 		if __debug__ and self.debug >= 5:
! 			_mesg('untagged_responses[%s] %s += %s' %
! 				(typ, len(ur.get(typ,'')), dat))
  		if ur.has_key(typ):
  			ur[typ].append(dat)
--- 566,575 ----
  	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)
***************
*** 557,560 ****
--- 578,587 ----
  
  
+ 	def _check_bye(self):
+ 		bye = self.untagged_responses.get('BYE')
+ 		if bye:
+ 			raise self.abort(bye[-1])
+ 
+ 
  	def _command(self, name, *args):
  
***************
*** 575,588 ****
  		tag = self._new_tag()
  		data = '%s %s' % (tag, name)
! 		for d in args:
! 			if d is None: continue
! 			if type(d) is type(''):
! 				l = len(string.split(d))
! 			else:
! 				l = 1
! 			if l == 0 or l > 1 and (d[0],d[-1]) not in (('(',')'),('"','"')):
! 				data = '%s "%s"' % (data, d)
! 			else:
! 				data = '%s %s' % (data, d)
  
  		literal = self.literal
--- 602,608 ----
  		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
***************
*** 595,598 ****
--- 615,624 ----
  				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))
***************
*** 600,606 ****
  			raise self.abort('socket error: %s' % val)
  
- 		if __debug__ and self.debug >= 4:
- 			_mesg('> %s' % data)
- 
  		if literal is None:
  			return tag
--- 626,629 ----
***************
*** 618,623 ****
  				literal = literator(self.continuation_response)
  
! 			if __debug__ and self.debug >= 4:
! 				_mesg('write literal size %s' % len(literal))
  
  			try:
--- 641,647 ----
  				literal = literator(self.continuation_response)
  
! 			if __debug__:
! 				if self.debug >= 4:
! 					_mesg('write literal size %s' % len(literal))
  
  			try:
***************
*** 634,637 ****
--- 658,662 ----
  
  	def _command_complete(self, name, tag):
+ 		self._check_bye()
  		try:
  			typ, data = self._get_tagged_response(tag)
***************
*** 640,645 ****
  		except self.error, val:
  			raise self.error('command: %s => %s' % (name, val))
! 		if self.untagged_responses.has_key('BYE') and name != 'LOGOUT':
! 			raise self.abort(self.untagged_responses['BYE'][-1])
  		if typ == 'BAD':
  			raise self.error('%s command error: %s %s' % (name, typ, data))
--- 665,669 ----
  		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))
***************
*** 696,701 ****
  
  				size = string.atoi(self.mo.group('size'))
! 				if __debug__ and self.debug >= 4:
! 					_mesg('read literal size %s' % size)
  				data = self.file.read(size)
  
--- 720,726 ----
  
  				size = string.atoi(self.mo.group('size'))
! 				if __debug__:
! 					if self.debug >= 4:
! 						_mesg('read literal size %s' % size)
  				data = self.file.read(size)
  
***************
*** 715,720 ****
  			self._append_untagged(self.mo.group('type'), self.mo.group('data'))
  
! 		if __debug__ and self.debug >= 1 and typ in ('NO', 'BAD'):
! 			_mesg('%s response: %s' % (typ, dat))
  
  		return resp
--- 740,746 ----
  			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
***************
*** 740,745 ****
  
  		line = line[:-2]
! 		if __debug__ and self.debug >= 4:
! 			_mesg('< %s' % line)
  		return line
  
--- 766,774 ----
  
  		line = line[:-2]
! 		if __debug__:
! 			if self.debug >= 4:
! 				_mesg('< %s' % line)
! 			else:
! 				_log('< %s' % line)
  		return line
  
***************
*** 751,756 ****
  
  		self.mo = cre.match(s)
! 		if __debug__ and 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
  
--- 780,786 ----
  
  		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
  
***************
*** 764,767 ****
--- 794,819 ----
  
  
+ 	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):
  
***************
*** 776,781 ****
  			return typ, [None]
  		data = self.untagged_responses[name]
! 		if __debug__ and self.debug >= 5:
! 			_mesg('untagged_responses[%s] => %s' % (name, data))
  		del self.untagged_responses[name]
  		return typ, data
--- 828,834 ----
  			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
***************
*** 902,906 ****
  
  	dttype = type(date_time)
! 	if dttype is type(1):
  		tt = time.localtime(date_time)
  	elif dttype is type(()):
--- 955,959 ----
  
  	dttype = type(date_time)
! 	if dttype is type(1) or dttype is type(1.1):
  		tt = time.localtime(date_time)
  	elif dttype is type(()):
***************
*** 923,929 ****
  if __debug__:
  
! 	def _mesg(s):
! #		if len(s) > 70: s = '%.70s..' % s
! 		sys.stderr.write('\t'+s+'\n')
  		sys.stderr.flush()
  
--- 976,984 ----
  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()
  
***************
*** 937,943 ****
  		_mesg('untagged responses dump:%s%s' % (t, j(l, t)))
  
  
  
! if __debug__ and __name__ == '__main__':
  
  	import getpass, sys
--- 992,1012 ----
  		_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
***************
*** 955,958 ****
--- 1024,1028 ----
  	('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)')),
***************
*** 969,972 ****
--- 1039,1043 ----
  	('uid', ('SEARCH', 'ALL')),
  	('response', ('EXISTS',)),
+ 	('append', (None, None, None, 'From: anon@x.y.z\n\ndata...')),
  	('recent', ()),
  	('logout', ()),
***************
*** 974,979 ****
  
  	def run(cmd, args):
  		typ, dat = apply(eval('M.%s' % cmd), args)
! 		_mesg(' %s %s\n  => %s %s' % (cmd, args, typ, dat))
  		return dat
  
--- 1045,1051 ----
  
  	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
  
***************
*** 997,1001 ****
  			continue
  
! 		uid = string.split(dat[-1])[-1]
! 		run('uid', ('FETCH', '%s' % uid,
  			'(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
--- 1069,1074 ----
  			continue
  
! 		uid = string.split(dat[-1])
! 		if not uid: continue
! 		run('uid', ('FETCH', '%s' % uid[-1],
  			'(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))