mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	 93a7c0fe6b
			
		
	
	
		93a7c0fe6b
		
	
	
	
	
		
			
			This fixes a bunch of socket.connect(host, post) calls. Note that I haven't tested all modules -- I don't have enough servers here...
		
			
				
	
	
		
			1111 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			1111 lines
		
	
	
	
		
			27 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| 
 | |
| """IMAP4 client.
 | |
| 
 | |
| Based on RFC 2060.
 | |
| 
 | |
| Public class:		IMAP4
 | |
| Public variable:	Debug
 | |
| Public functions:	Internaldate2tuple
 | |
| 			Int2AP
 | |
| 			ParseFlags
 | |
| 			Time2Internaldate
 | |
| """
 | |
| 
 | |
| # Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
 | |
| # 
 | |
| # Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
 | |
| 
 | |
| __version__ = "2.36"
 | |
| 
 | |
| 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<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!#$%&'*+,.:;<=>?^`|~-]")
 | |
| 
 | |
| 	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.is_readonly = None		# READ-ONLY desired state
 | |
| 		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, criterium, ...)
 | |
| 
 | |
| 		'data' is space separated list of matching message numbers.
 | |
| 		"""
 | |
| 		name = 'SEARCH'
 | |
| 		if charset:
 | |
| 			charset = 'CHARSET ' + charset
 | |
| 		typ, dat = apply(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.
 | |
| 		self.is_readonly = readonly
 | |
| 		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 self.untagged_responses.has_key('READ-ONLY') \
 | |
| 			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-ONLY') \
 | |
| 		and not self.is_readonly:
 | |
| 			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) response...
 | |
| 
 | |
| 				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
 | |
| 
 | |
| 			# Some have reported "unexpected response" exceptions.
 | |
| 			# Note that ignoring them here causes loops.
 | |
| 			# Instead, send me details of the unexpected response and
 | |
| 			# I'll update the code in `_get_response()'.
 | |
| 
 | |
| 			try:
 | |
| 				self._get_response()
 | |
| 			except self.abort, val:
 | |
| 				if __debug__:
 | |
| 					if self.debug >= 1:
 | |
| 						print_log()
 | |
| 				raise
 | |
| 
 | |
| 
 | |
| 	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 getopt, getpass, sys
 | |
| 
 | |
| 	try:
 | |
| 		optlist, args = getopt.getopt(sys.argv[1:], 'd:')
 | |
| 	except getopt.error, val:
 | |
| 		pass
 | |
| 
 | |
| 	for opt,val in optlist:
 | |
| 		if opt == '-d':
 | |
| 			Debug = int(val)
 | |
| 
 | |
| 	if not args: args = ('',)
 | |
| 
 | |
| 	host = args[0]
 | |
| 
 | |
| 	USER = getpass.getuser()
 | |
| 	PASSWD = getpass.getpass("IMAP password for %s on %s" % (USER, host or "localhost"))
 | |
| 
 | |
| 	test_mesg = 'From: %s@localhost\nSubject: IMAP4 test\n\ndata...\n' % USER
 | |
| 	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, test_mesg)),
 | |
| 	('list', ('/tmp', 'yy*')),
 | |
| 	('select', ('/tmp/yyz 2',)),
 | |
| 	('search', (None, 'SUBJECT', 'test')),
 | |
| 	('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, test_mesg)),
 | |
| 	('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
 | |
| 
 | |
| 	try:
 | |
| 		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)'))
 | |
| 
 | |
| 		print '\nAll tests OK.'
 | |
| 
 | |
| 	except:
 | |
| 		print '\nTests failed.'
 | |
| 
 | |
| 		if not Debug:
 | |
| 			print '''
 | |
| If you would like to see debugging output,
 | |
| try: %s -d5
 | |
| ''' % sys.argv[0]
 | |
| 
 | |
| 		raise
 |