1515 Time2Internaldate
1616"""
1717
18- __version__ = "2.15 "
18+ __version__ = "2.16 "
1919
2020import 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 ("\t matched r'%s' => %s" % (cre .pattern , `self.mo.groups()` ))
782+ if __debug__ :
783+ if self .mo is not None and self .debug >= 5 :
784+ _mesg ("\t matched 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
923976if __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 \n data...' )),
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 \n data...' )),
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