Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8c06221

Browse files
committed
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.
1 parent 97798b1 commit 8c06221

1 file changed

Lines changed: 123 additions & 50 deletions

File tree

Lib/imaplib.py

Lines changed: 123 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
Time2Internaldate
1616
"""
1717

18-
__version__ = "2.15"
18+
__version__ = "2.16"
1919

2020
import binascii, re, socket, string, time, random, sys
2121

@@ -89,7 +89,8 @@ class IMAP4:
8989
AUTHENTICATE, and the last argument to APPEND which is passed as
9090
an IMAP4 literal. If necessary (the string contains
9191
white-space and isn't enclosed with either parentheses or
92-
double quotes) each string is quoted.
92+
double quotes) each string is quoted. However, the 'password'
93+
argument to the LOGIN command is always quoted.
9394
9495
Each command returns a tuple: (type, [data, ...]) where 'type'
9596
is usually 'OK' or 'NO', and 'data' is either the text from the
@@ -101,6 +102,11 @@ class IMAP4:
101102
from READ-WRITE to READ-ONLY raise the exception class
102103
<instance>.readonly("<reason>"), which is a sub-class of 'abort'.
103104
105+
"error" exceptions imply a program error.
106+
"abort" exceptions imply the connection should be reset, and
107+
the command re-tried.
108+
"readonly" exceptions imply the command should be re-tried.
109+
104110
Note: to use this module, you must read the RFCs pertaining
105111
to the IMAP4 protocol, as the semantics of the arguments to
106112
each IMAP4 command are left to the invoker, not to mention
@@ -111,6 +117,7 @@ class error(Exception): pass # Logical errors - debug required
111117
class abort(error): pass # Service errors - close and retry
112118
class readonly(abort): pass # Mailbox status changed to READ-ONLY
113119

120+
mustquote = re.compile(r'\W') # Match any non-alphanumeric character
114121

115122
def __init__(self, host = '', port = IMAP4_PORT):
116123
self.host = host
@@ -138,15 +145,15 @@ def __init__(self, host = '', port = IMAP4_PORT):
138145
# Get server welcome message,
139146
# request and store CAPABILITY response.
140147

141-
if __debug__ and self.debug >= 1:
142-
_mesg('new IMAP4 connection, tag=%s' % self.tagpre)
148+
if __debug__:
149+
if self.debug >= 1:
150+
_mesg('new IMAP4 connection, tag=%s' % self.tagpre)
143151

144152
self.welcome = self._get_response()
145153
if self.untagged_responses.has_key('PREAUTH'):
146154
self.state = 'AUTH'
147155
elif self.untagged_responses.has_key('OK'):
148156
self.state = 'NONAUTH'
149-
# elif self.untagged_responses.has_key('BYE'):
150157
else:
151158
raise self.error(self.welcome)
152159

@@ -156,8 +163,9 @@ def __init__(self, host = '', port = IMAP4_PORT):
156163
raise self.error('no CAPABILITY response from server')
157164
self.capabilities = tuple(string.split(string.upper(self.untagged_responses[cap][-1])))
158165

159-
if __debug__ and self.debug >= 3:
160-
_mesg('CAPABILITIES: %s' % `self.capabilities`)
166+
if __debug__:
167+
if self.debug >= 3:
168+
_mesg('CAPABILITIES: %s' % `self.capabilities`)
161169

162170
for version in AllowedVersions:
163171
if not version in self.capabilities:
@@ -229,8 +237,12 @@ def append(self, mailbox, flags, date_time, message):
229237
"""Append message to named mailbox.
230238
231239
(typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
240+
241+
All args except `message' can be None.
232242
"""
233243
name = 'APPEND'
244+
if not mailbox:
245+
mailbox = 'INBOX'
234246
if flags:
235247
if (flags[0],flags[-1]) != ('(',')'):
236248
flags = '(%s)' % flags
@@ -360,9 +372,13 @@ def list(self, directory='""', pattern='*'):
360372
def login(self, user, password):
361373
"""Identify client using plaintext password.
362374
363-
(typ, [data]) = <instance>.list(user, password)
375+
(typ, [data]) = <instance>.login(user, password)
376+
377+
NB: 'password' will be quoted.
364378
"""
365-
typ, dat = self._simple_command('LOGIN', user, password)
379+
#if not 'AUTH=LOGIN' in self.capabilities:
380+
# raise self.error("Server doesn't allow LOGIN authentication." % mech)
381+
typ, dat = self._simple_command('LOGIN', user, self._quote(password))
366382
if typ != 'OK':
367383
raise self.error(dat[-1])
368384
self.state = 'AUTH'
@@ -403,8 +419,9 @@ def noop(self):
403419
404420
(typ, data) = <instance>.noop()
405421
"""
406-
if __debug__ and self.debug >= 3:
407-
_dump_ur(self.untagged_responses)
422+
if __debug__:
423+
if self.debug >= 3:
424+
_dump_ur(self.untagged_responses)
408425
return self._simple_command('NOOP')
409426

410427

@@ -464,7 +481,9 @@ def select(self, mailbox='INBOX', readonly=None):
464481
self.state = 'SELECTED'
465482
if not self.untagged_responses.has_key('READ-WRITE') \
466483
and not readonly:
467-
if __debug__ and self.debug >= 1: _dump_ur(self.untagged_responses)
484+
if __debug__:
485+
if self.debug >= 1:
486+
_dump_ur(self.untagged_responses)
468487
raise self.readonly('%s is not writable' % mailbox)
469488
return typ, self.untagged_responses.get('EXISTS', [None])
470489

@@ -546,16 +565,24 @@ def xatom(self, name, *args):
546565

547566
def _append_untagged(self, typ, dat):
548567

568+
if dat is None: dat = ''
549569
ur = self.untagged_responses
550-
if __debug__ and self.debug >= 5:
551-
_mesg('untagged_responses[%s] %s += %s' %
552-
(typ, len(ur.get(typ,'')), dat))
570+
if __debug__:
571+
if self.debug >= 5:
572+
_mesg('untagged_responses[%s] %s += ["%s"]' %
573+
(typ, len(ur.get(typ,'')), dat))
553574
if ur.has_key(typ):
554575
ur[typ].append(dat)
555576
else:
556577
ur[typ] = [dat]
557578

558579

580+
def _check_bye(self):
581+
bye = self.untagged_responses.get('BYE')
582+
if bye:
583+
raise self.abort(bye[-1])
584+
585+
559586
def _command(self, name, *args):
560587

561588
if self.state not in Commands[name]:
@@ -574,16 +601,9 @@ def _command(self, name, *args):
574601

575602
tag = self._new_tag()
576603
data = '%s %s' % (tag, name)
577-
for d in args:
578-
if d is None: continue
579-
if type(d) is type(''):
580-
l = len(string.split(d))
581-
else:
582-
l = 1
583-
if l == 0 or l > 1 and (d[0],d[-1]) not in (('(',')'),('"','"')):
584-
data = '%s "%s"' % (data, d)
585-
else:
586-
data = '%s %s' % (data, d)
604+
for arg in args:
605+
if arg is None: continue
606+
data = '%s %s' % (data, self._checkquote(arg))
587607

588608
literal = self.literal
589609
if literal is not None:
@@ -594,14 +614,17 @@ def _command(self, name, *args):
594614
literator = None
595615
data = '%s {%s}' % (data, len(literal))
596616

617+
if __debug__:
618+
if self.debug >= 4:
619+
_mesg('> %s' % data)
620+
else:
621+
_log('> %s' % data)
622+
597623
try:
598624
self.sock.send('%s%s' % (data, CRLF))
599625
except socket.error, val:
600626
raise self.abort('socket error: %s' % val)
601627

602-
if __debug__ and self.debug >= 4:
603-
_mesg('> %s' % data)
604-
605628
if literal is None:
606629
return tag
607630

@@ -617,8 +640,9 @@ def _command(self, name, *args):
617640
if literator:
618641
literal = literator(self.continuation_response)
619642

620-
if __debug__ and self.debug >= 4:
621-
_mesg('write literal size %s' % len(literal))
643+
if __debug__:
644+
if self.debug >= 4:
645+
_mesg('write literal size %s' % len(literal))
622646

623647
try:
624648
self.sock.send(literal)
@@ -633,14 +657,14 @@ def _command(self, name, *args):
633657

634658

635659
def _command_complete(self, name, tag):
660+
self._check_bye()
636661
try:
637662
typ, data = self._get_tagged_response(tag)
638663
except self.abort, val:
639664
raise self.abort('command: %s => %s' % (name, val))
640665
except self.error, val:
641666
raise self.error('command: %s => %s' % (name, val))
642-
if self.untagged_responses.has_key('BYE') and name != 'LOGOUT':
643-
raise self.abort(self.untagged_responses['BYE'][-1])
667+
self._check_bye()
644668
if typ == 'BAD':
645669
raise self.error('%s command error: %s %s' % (name, typ, data))
646670
return typ, data
@@ -695,8 +719,9 @@ def _get_response(self):
695719
# Read literal direct from connection.
696720

697721
size = string.atoi(self.mo.group('size'))
698-
if __debug__ and self.debug >= 4:
699-
_mesg('read literal size %s' % size)
722+
if __debug__:
723+
if self.debug >= 4:
724+
_mesg('read literal size %s' % size)
700725
data = self.file.read(size)
701726

702727
# Store response with literal as tuple
@@ -714,8 +739,9 @@ def _get_response(self):
714739
if typ in ('OK', 'NO', 'BAD') and self._match(Response_code, dat):
715740
self._append_untagged(self.mo.group('type'), self.mo.group('data'))
716741

717-
if __debug__ and self.debug >= 1 and typ in ('NO', 'BAD'):
718-
_mesg('%s response: %s' % (typ, dat))
742+
if __debug__:
743+
if self.debug >= 1 and typ in ('NO', 'BAD', 'BYE'):
744+
_mesg('%s response: %s' % (typ, dat))
719745

720746
return resp
721747

@@ -739,8 +765,11 @@ def _get_line(self):
739765
# Protocol mandates all lines terminated by CRLF
740766

741767
line = line[:-2]
742-
if __debug__ and self.debug >= 4:
743-
_mesg('< %s' % line)
768+
if __debug__:
769+
if self.debug >= 4:
770+
_mesg('< %s' % line)
771+
else:
772+
_log('< %s' % line)
744773
return line
745774

746775

@@ -750,8 +779,9 @@ def _match(self, cre, s):
750779
# Save result, return success.
751780

752781
self.mo = cre.match(s)
753-
if __debug__ and self.mo is not None and self.debug >= 5:
754-
_mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
782+
if __debug__:
783+
if self.mo is not None and self.debug >= 5:
784+
_mesg("\tmatched r'%s' => %s" % (cre.pattern, `self.mo.groups()`))
755785
return self.mo is not None
756786

757787

@@ -763,6 +793,28 @@ def _new_tag(self):
763793
return tag
764794

765795

796+
def _checkquote(self, arg):
797+
798+
# Must quote command args if non-alphanumeric chars present,
799+
# and not already quoted.
800+
801+
if type(arg) is not type(''):
802+
return arg
803+
if (arg[0],arg[-1]) in (('(',')'),('"','"')):
804+
return arg
805+
if self.mustquote.search(arg) is None:
806+
return arg
807+
return self._quote(arg)
808+
809+
810+
def _quote(self, arg):
811+
812+
arg = string.replace(arg, '\\', '\\\\')
813+
arg = string.replace(arg, '"', '\\"')
814+
815+
return '"%s"' % arg
816+
817+
766818
def _simple_command(self, name, *args):
767819

768820
return self._command_complete(name, apply(self._command, (name,) + args))
@@ -775,8 +827,9 @@ def _untagged_response(self, typ, dat, name):
775827
if not self.untagged_responses.has_key(name):
776828
return typ, [None]
777829
data = self.untagged_responses[name]
778-
if __debug__ and self.debug >= 5:
779-
_mesg('untagged_responses[%s] => %s' % (name, data))
830+
if __debug__:
831+
if self.debug >= 5:
832+
_mesg('untagged_responses[%s] => %s' % (name, data))
780833
del self.untagged_responses[name]
781834
return typ, data
782835

@@ -901,7 +954,7 @@ def Time2Internaldate(date_time):
901954
"""
902955

903956
dttype = type(date_time)
904-
if dttype is type(1):
957+
if dttype is type(1) or dttype is type(1.1):
905958
tt = time.localtime(date_time)
906959
elif dttype is type(()):
907960
tt = date_time
@@ -922,9 +975,11 @@ def Time2Internaldate(date_time):
922975

923976
if __debug__:
924977

925-
def _mesg(s):
926-
# if len(s) > 70: s = '%.70s..' % s
927-
sys.stderr.write('\t'+s+'\n')
978+
def _mesg(s, secs=None):
979+
if secs is None:
980+
secs = time.time()
981+
tm = time.strftime('%M:%S', time.localtime(secs))
982+
sys.stderr.write(' %s.%02d %s\n' % (tm, (secs*100)%100, s))
928983
sys.stderr.flush()
929984

930985
def _dump_ur(dict):
@@ -936,9 +991,23 @@ def _dump_ur(dict):
936991
l = map(lambda x,j=j:'%s: "%s"' % (x[0], x[1][0] and j(x[1], '" "') or ''), l)
937992
_mesg('untagged responses dump:%s%s' % (t, j(l, t)))
938993

994+
_cmd_log = [] # Last `_cmd_log_len' interactions
995+
_cmd_log_len = 10
996+
997+
def _log(line):
998+
# Keep log of last `_cmd_log_len' interactions for debugging.
999+
if len(_cmd_log) == _cmd_log_len:
1000+
del _cmd_log[0]
1001+
_cmd_log.append((time.time(), line))
1002+
1003+
def print_log():
1004+
_mesg('last %d IMAP4 interactions:' % len(_cmd_log))
1005+
for secs,line in _cmd_log:
1006+
_mesg(line, secs)
1007+
9391008

9401009

941-
if __debug__ and __name__ == '__main__':
1010+
if __name__ == '__main__':
9421011

9431012
import getpass, sys
9441013

@@ -954,6 +1023,7 @@ def _dump_ur(dict):
9541023
('rename', ('/tmp/xxx 1', '/tmp/yyy')),
9551024
('CREATE', ('/tmp/yyz 2',)),
9561025
('append', ('/tmp/yyz 2', None, None, 'From: [email protected]\n\ndata...')),
1026+
('list', ('/tmp', 'yy*')),
9571027
('select', ('/tmp/yyz 2',)),
9581028
('search', (None, '(TO zork)')),
9591029
('partial', ('1', 'RFC822', 1, 1024)),
@@ -968,13 +1038,15 @@ def _dump_ur(dict):
9681038
('response',('UIDVALIDITY',)),
9691039
('uid', ('SEARCH', 'ALL')),
9701040
('response', ('EXISTS',)),
1041+
('append', (None, None, None, 'From: [email protected]\n\ndata...')),
9711042
('recent', ()),
9721043
('logout', ()),
9731044
)
9741045

9751046
def run(cmd, args):
1047+
_mesg('%s %s' % (cmd, args))
9761048
typ, dat = apply(eval('M.%s' % cmd), args)
977-
_mesg(' %s %s\n => %s %s' % (cmd, args, typ, dat))
1049+
_mesg('%s => %s %s' % (cmd, typ, dat))
9781050
return dat
9791051

9801052
Debug = 5
@@ -996,6 +1068,7 @@ def run(cmd, args):
9961068
if (cmd,args) != ('uid', ('SEARCH', 'ALL')):
9971069
continue
9981070

999-
uid = string.split(dat[-1])[-1]
1000-
run('uid', ('FETCH', '%s' % uid,
1071+
uid = string.split(dat[-1])
1072+
if not uid: continue
1073+
run('uid', ('FETCH', '%s' % uid[-1],
10011074
'(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))

0 commit comments

Comments
 (0)