7 Public functions: Internaldate2tuple
13 # Author: Piers Lauder <piers@cs.su.oz.au> December 1997.
15 # Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
16 # String method conversion by ESR, February 2001.
17 # GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
21 import binascii
, re
, socket
, time
, random
, sys
23 __all__
= ["IMAP4", "Internaldate2tuple",
24 "Int2AP", "ParseFlags", "Time2Internaldate"]
31 AllowedVersions
= ('IMAP4REV1', 'IMAP4') # Most recent first
37 'APPEND': ('AUTH', 'SELECTED'),
38 'AUTHENTICATE': ('NONAUTH',),
39 'CAPABILITY': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
40 'CHECK': ('SELECTED',),
41 'CLOSE': ('SELECTED',),
42 'COPY': ('SELECTED',),
43 'CREATE': ('AUTH', 'SELECTED'),
44 'DELETE': ('AUTH', 'SELECTED'),
45 'EXAMINE': ('AUTH', 'SELECTED'),
46 'EXPUNGE': ('SELECTED',),
47 'FETCH': ('SELECTED',),
48 'GETACL': ('AUTH', 'SELECTED'),
49 'LIST': ('AUTH', 'SELECTED'),
50 'LOGIN': ('NONAUTH',),
51 'LOGOUT': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
52 'LSUB': ('AUTH', 'SELECTED'),
53 'NAMESPACE': ('AUTH', 'SELECTED'),
54 'NOOP': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'),
55 'PARTIAL': ('SELECTED',),
56 'RENAME': ('AUTH', 'SELECTED'),
57 'SEARCH': ('SELECTED',),
58 'SELECT': ('AUTH', 'SELECTED'),
59 'SETACL': ('AUTH', 'SELECTED'),
60 'SORT': ('SELECTED',),
61 'STATUS': ('AUTH', 'SELECTED'),
62 'STORE': ('SELECTED',),
63 'SUBSCRIBE': ('AUTH', 'SELECTED'),
65 'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
68 # Patterns to match server responses
70 Continuation
= re
.compile(r
'\+( (?P<data>.*))?')
71 Flags
= re
.compile(r
'.*FLAGS \((?P<flags>[^\)]*)\)')
72 InternalDate
= re
.compile(r
'.*INTERNALDATE "'
73 r
'(?P<day>[ 123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
74 r
' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
75 r
' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
77 Literal
= re
.compile(r
'.*{(?P<size>\d+)}$')
78 Response_code
= re
.compile(r
'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
79 Untagged_response
= re
.compile(r
'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
80 Untagged_status
= re
.compile(r
'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
86 """IMAP4 client class.
88 Instantiate with: IMAP4([host[, port]])
90 host - host's name (default: localhost);
91 port - port number (default: standard IMAP4 port).
93 All IMAP4rev1 commands are supported by methods of the same
96 All arguments to commands are converted to strings, except for
97 AUTHENTICATE, and the last argument to APPEND which is passed as
98 an IMAP4 literal. If necessary (the string contains any
99 non-printing characters or white-space and isn't enclosed with
100 either parentheses or double quotes) each string is quoted.
101 However, the 'password' argument to the LOGIN command is always
102 quoted. If you want to avoid having an argument string quoted
103 (eg: the 'flags' argument to STORE) then enclose the string in
104 parentheses (eg: "(\Deleted)").
106 Each command returns a tuple: (type, [data, ...]) where 'type'
107 is usually 'OK' or 'NO', and 'data' is either the text from the
108 tagged response, or untagged results from command.
110 Errors raise the exception class <instance>.error("<reason>").
111 IMAP4 server errors raise <instance>.abort("<reason>"),
112 which is a sub-class of 'error'. Mailbox status changes
113 from READ-WRITE to READ-ONLY raise the exception class
114 <instance>.readonly("<reason>"), which is a sub-class of 'abort'.
116 "error" exceptions imply a program error.
117 "abort" exceptions imply the connection should be reset, and
118 the command re-tried.
119 "readonly" exceptions imply the command should be re-tried.
121 Note: to use this module, you must read the RFCs pertaining
122 to the IMAP4 protocol, as the semantics of the arguments to
123 each IMAP4 command are left to the invoker, not to mention
127 class error(Exception): pass # Logical errors - debug required
128 class abort(error
): pass # Service errors - close and retry
129 class readonly(abort
): pass # Mailbox status changed to READ-ONLY
131 mustquote
= re
.compile(r
"[^\w!#$%&'*+,.:;<=>?^`|~-]")
133 def __init__(self
, host
= '', port
= IMAP4_PORT
):
137 self
.state
= 'LOGOUT'
138 self
.literal
= None # A literal argument to a command
139 self
.tagged_commands
= {} # Tagged commands awaiting response
140 self
.untagged_responses
= {} # {typ: [data, ...], ...}
141 self
.continuation_response
= '' # Last continuation response
142 self
.is_readonly
= None # READ-ONLY desired state
145 # Open socket to server.
147 self
.open(host
, port
)
149 # Create unique tag for this session,
150 # and compile tagged response matcher.
152 self
.tagpre
= Int2AP(random
.randint(0, 31999))
153 self
.tagre
= re
.compile(r
'(?P<tag>'
155 + r
'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
157 # Get server welcome message,
158 # request and store CAPABILITY response.
162 _mesg('imaplib version %s' % __version__
)
163 _mesg('new IMAP4 connection, tag=%s' % self
.tagpre
)
165 self
.welcome
= self
._get
_response
()
166 if self
.untagged_responses
.has_key('PREAUTH'):
168 elif self
.untagged_responses
.has_key('OK'):
169 self
.state
= 'NONAUTH'
171 raise self
.error(self
.welcome
)
174 self
._simple
_command
(cap
)
175 if not self
.untagged_responses
.has_key(cap
):
176 raise self
.error('no CAPABILITY response from server')
177 self
.capabilities
= tuple(self
.untagged_responses
[cap
][-1].upper().split())
181 _mesg('CAPABILITIES: %s' % `self
.capabilities`
)
183 for version
in AllowedVersions
:
184 if not version
in self
.capabilities
:
186 self
.PROTOCOL_VERSION
= version
189 raise self
.error('server not IMAP4 compliant')
192 def __getattr__(self
, attr
):
193 # Allow UPPERCASE variants of IMAP4 command methods.
194 if Commands
.has_key(attr
):
195 return getattr(self
, attr
.lower())
196 raise AttributeError("Unknown IMAP4 command: '%s'" % attr
)
200 # Overridable methods
203 def open(self
, host
, port
):
204 """Setup connection to remote server on "host:port".
205 This connection will be used by the routines:
206 read, readline, send, shutdown.
208 self
.sock
= socket
.socket(socket
.AF_INET
, socket
.SOCK_STREAM
)
209 self
.sock
.connect((self
.host
, self
.port
))
210 self
.file = self
.sock
.makefile('rb')
213 def read(self
, size
):
214 """Read 'size' bytes from remote."""
215 return self
.file.read(size
)
219 """Read line from remote."""
220 return self
.file.readline()
223 def send(self
, data
):
224 """Send data to remote."""
227 sent
= self
.sock
.send(data
)
235 """Close I/O established in "open"."""
241 """Return socket instance used to connect to IMAP4 server.
243 socket = <instance>.socket()
253 """Return most recent 'RECENT' responses if any exist,
254 else prompt server for an update using the 'NOOP' command.
256 (typ, [data]) = <instance>.recent()
258 'data' is None if no new messages,
259 else list of RECENT responses, most recent last.
262 typ
, dat
= self
._untagged
_response
('OK', [None], name
)
265 typ
, dat
= self
.noop() # Prod server for response
266 return self
._untagged
_response
(typ
, dat
, name
)
269 def response(self
, code
):
270 """Return data for response 'code' if received, or None.
272 Old value for response 'code' is cleared.
274 (code, [data]) = <instance>.response(code)
276 return self
._untagged
_response
(code
, [None], code
.upper())
283 def append(self
, mailbox
, flags
, date_time
, message
):
284 """Append message to named mailbox.
286 (typ, [data]) = <instance>.append(mailbox, flags, date_time, message)
288 All args except `message' can be None.
294 if (flags
[0],flags
[-1]) != ('(',')'):
295 flags
= '(%s)' % flags
299 date_time
= Time2Internaldate(date_time
)
302 self
.literal
= message
303 return self
._simple
_command
(name
, mailbox
, flags
, date_time
)
306 def authenticate(self
, mechanism
, authobject
):
307 """Authenticate command - requires response processing.
309 'mechanism' specifies which authentication mechanism is to
310 be used - it must appear in <instance>.capabilities in the
311 form AUTH=<mechanism>.
313 'authobject' must be a callable object:
315 data = authobject(response)
317 It will be called to process server continuation responses.
318 It should return data that will be encoded and sent to server.
319 It should return None if the client abort response '*' should
322 mech
= mechanism
.upper()
323 cap
= 'AUTH=%s' % mech
324 if not cap
in self
.capabilities
:
325 raise self
.error("Server doesn't allow %s authentication." % mech
)
326 self
.literal
= _Authenticator(authobject
).process
327 typ
, dat
= self
._simple
_command
('AUTHENTICATE', mech
)
329 raise self
.error(dat
[-1])
335 """Checkpoint mailbox on server.
337 (typ, [data]) = <instance>.check()
339 return self
._simple
_command
('CHECK')
343 """Close currently selected mailbox.
345 Deleted messages are removed from writable mailbox.
346 This is the recommended command before 'LOGOUT'.
348 (typ, [data]) = <instance>.close()
351 typ
, dat
= self
._simple
_command
('CLOSE')
357 def copy(self
, message_set
, new_mailbox
):
358 """Copy 'message_set' messages onto end of 'new_mailbox'.
360 (typ, [data]) = <instance>.copy(message_set, new_mailbox)
362 return self
._simple
_command
('COPY', message_set
, new_mailbox
)
365 def create(self
, mailbox
):
366 """Create new mailbox.
368 (typ, [data]) = <instance>.create(mailbox)
370 return self
._simple
_command
('CREATE', mailbox
)
373 def delete(self
, mailbox
):
374 """Delete old mailbox.
376 (typ, [data]) = <instance>.delete(mailbox)
378 return self
._simple
_command
('DELETE', mailbox
)
382 """Permanently remove deleted items from selected mailbox.
384 Generates 'EXPUNGE' response for each deleted message.
386 (typ, [data]) = <instance>.expunge()
388 'data' is list of 'EXPUNGE'd message numbers in order received.
391 typ
, dat
= self
._simple
_command
(name
)
392 return self
._untagged
_response
(typ
, dat
, name
)
395 def fetch(self
, message_set
, message_parts
):
396 """Fetch (parts of) messages.
398 (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
400 'message_parts' should be a string of selected parts
401 enclosed in parentheses, eg: "(UID BODY[TEXT])".
403 'data' are tuples of message part envelope and data.
406 typ
, dat
= self
._simple
_command
(name
, message_set
, message_parts
)
407 return self
._untagged
_response
(typ
, dat
, name
)
410 def getacl(self
, mailbox
):
411 """Get the ACLs for a mailbox.
413 (typ, [data]) = <instance>.getacl(mailbox)
415 typ
, dat
= self
._simple
_command
('GETACL', mailbox
)
416 return self
._untagged
_response
(typ
, dat
, 'ACL')
419 def list(self
, directory
='""', pattern
='*'):
420 """List mailbox names in directory matching pattern.
422 (typ, [data]) = <instance>.list(directory='""', pattern='*')
424 'data' is list of LIST responses.
427 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
428 return self
._untagged
_response
(typ
, dat
, name
)
431 def login(self
, user
, password
):
432 """Identify client using plaintext password.
434 (typ, [data]) = <instance>.login(user, password)
436 NB: 'password' will be quoted.
438 #if not 'AUTH=LOGIN' in self.capabilities:
439 # raise self.error("Server doesn't allow LOGIN authentication." % mech)
440 typ
, dat
= self
._simple
_command
('LOGIN', user
, self
._quote
(password
))
442 raise self
.error(dat
[-1])
448 """Shutdown connection to server.
450 (typ, [data]) = <instance>.logout()
452 Returns server 'BYE' response.
454 self
.state
= 'LOGOUT'
455 try: typ
, dat
= self
._simple
_command
('LOGOUT')
456 except: typ
, dat
= 'NO', ['%s: %s' % sys
.exc_info()[:2]]
458 if self
.untagged_responses
.has_key('BYE'):
459 return 'BYE', self
.untagged_responses
['BYE']
463 def lsub(self
, directory
='""', pattern
='*'):
464 """List 'subscribed' mailbox names in directory matching pattern.
466 (typ, [data, ...]) = <instance>.lsub(directory='""', pattern='*')
468 'data' are tuples of message part envelope and data.
471 typ
, dat
= self
._simple
_command
(name
, directory
, pattern
)
472 return self
._untagged
_response
(typ
, dat
, name
)
476 """ Returns IMAP namespaces ala rfc2342
478 (typ, [data, ...]) = <instance>.namespace()
481 typ
, dat
= self
._simple
_command
(name
)
482 return self
._untagged
_response
(typ
, dat
, name
)
486 """Send NOOP command.
488 (typ, data) = <instance>.noop()
492 _dump_ur(self
.untagged_responses
)
493 return self
._simple
_command
('NOOP')
496 def partial(self
, message_num
, message_part
, start
, length
):
497 """Fetch truncated part of a message.
499 (typ, [data, ...]) = <instance>.partial(message_num, message_part, start, length)
501 'data' is tuple of message part envelope and data.
504 typ
, dat
= self
._simple
_command
(name
, message_num
, message_part
, start
, length
)
505 return self
._untagged
_response
(typ
, dat
, 'FETCH')
508 def rename(self
, oldmailbox
, newmailbox
):
509 """Rename old mailbox name to new.
511 (typ, data) = <instance>.rename(oldmailbox, newmailbox)
513 return self
._simple
_command
('RENAME', oldmailbox
, newmailbox
)
516 def search(self
, charset
, *criteria
):
517 """Search mailbox for matching messages.
519 (typ, [data]) = <instance>.search(charset, criterium, ...)
521 'data' is space separated list of matching message numbers.
525 typ
, dat
= apply(self
._simple
_command
, (name
, 'CHARSET', charset
) + criteria
)
527 typ
, dat
= apply(self
._simple
_command
, (name
,) + criteria
)
528 return self
._untagged
_response
(typ
, dat
, name
)
531 def select(self
, mailbox
='INBOX', readonly
=None):
534 Flush all untagged responses.
536 (typ, [data]) = <instance>.select(mailbox='INBOX', readonly=None)
538 'data' is count of messages in mailbox ('EXISTS' response).
540 # Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY')
541 self
.untagged_responses
= {} # Flush old responses.
542 self
.is_readonly
= readonly
547 typ
, dat
= self
._simple
_command
(name
, mailbox
)
549 self
.state
= 'AUTH' # Might have been 'SELECTED'
551 self
.state
= 'SELECTED'
552 if self
.untagged_responses
.has_key('READ-ONLY') \
556 _dump_ur(self
.untagged_responses
)
557 raise self
.readonly('%s is not writable' % mailbox
)
558 return typ
, self
.untagged_responses
.get('EXISTS', [None])
561 def setacl(self
, mailbox
, who
, what
):
562 """Set a mailbox acl.
564 (typ, [data]) = <instance>.create(mailbox, who, what)
566 return self
._simple
_command
('SETACL', mailbox
, who
, what
)
569 def sort(self
, sort_criteria
, charset
, *search_criteria
):
570 """IMAP4rev1 extension SORT command.
572 (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
575 #if not name in self.capabilities: # Let the server decide!
576 # raise self.error('unimplemented extension command: %s' % name)
577 if (sort_criteria
[0],sort_criteria
[-1]) != ('(',')'):
578 sort_criteria
= '(%s)' % sort_criteria
579 typ
, dat
= apply(self
._simple
_command
, (name
, sort_criteria
, charset
) + search_criteria
)
580 return self
._untagged
_response
(typ
, dat
, name
)
583 def status(self
, mailbox
, names
):
584 """Request named status conditions for mailbox.
586 (typ, [data]) = <instance>.status(mailbox, names)
589 #if self.PROTOCOL_VERSION == 'IMAP4': # Let the server decide!
590 # raise self.error('%s unimplemented in IMAP4 (obtain IMAP4rev1 server, or re-code)' % name)
591 typ
, dat
= self
._simple
_command
(name
, mailbox
, names
)
592 return self
._untagged
_response
(typ
, dat
, name
)
595 def store(self
, message_set
, command
, flags
):
596 """Alters flag dispositions for messages in mailbox.
598 (typ, [data]) = <instance>.store(message_set, command, flags)
600 if (flags
[0],flags
[-1]) != ('(',')'):
601 flags
= '(%s)' % flags
# Avoid quoting the flags
602 typ
, dat
= self
._simple
_command
('STORE', message_set
, command
, flags
)
603 return self
._untagged
_response
(typ
, dat
, 'FETCH')
606 def subscribe(self
, mailbox
):
607 """Subscribe to new mailbox.
609 (typ, [data]) = <instance>.subscribe(mailbox)
611 return self
._simple
_command
('SUBSCRIBE', mailbox
)
614 def uid(self
, command
, *args
):
615 """Execute "command arg ..." with messages identified by UID,
616 rather than message number.
618 (typ, [data]) = <instance>.uid(command, arg1, arg2, ...)
620 Returns response appropriate to 'command'.
622 command
= command
.upper()
623 if not Commands
.has_key(command
):
624 raise self
.error("Unknown IMAP4 UID command: %s" % command
)
625 if self
.state
not in Commands
[command
]:
626 raise self
.error('command %s illegal in state %s'
627 % (command
, self
.state
))
629 typ
, dat
= apply(self
._simple
_command
, (name
, command
) + args
)
630 if command
in ('SEARCH', 'SORT'):
634 return self
._untagged
_response
(typ
, dat
, name
)
637 def unsubscribe(self
, mailbox
):
638 """Unsubscribe from old mailbox.
640 (typ, [data]) = <instance>.unsubscribe(mailbox)
642 return self
._simple
_command
('UNSUBSCRIBE', mailbox
)
645 def xatom(self
, name
, *args
):
646 """Allow simple extension commands
647 notified by server in CAPABILITY response.
649 Assumes command is legal in current state.
651 (typ, [data]) = <instance>.xatom(name, arg, ...)
653 Returns response appropriate to extension command `name'.
656 #if not name in self.capabilities: # Let the server decide!
657 # raise self.error('unknown extension command: %s' % name)
658 if not Commands
.has_key(name
):
659 Commands
[name
] = (self
.state
,)
660 return apply(self
._simple
_command
, (name
,) + args
)
667 def _append_untagged(self
, typ
, dat
):
669 if dat
is None: dat
= ''
670 ur
= self
.untagged_responses
673 _mesg('untagged_responses[%s] %s += ["%s"]' %
674 (typ
, len(ur
.get(typ
,'')), dat
))
681 def _check_bye(self
):
682 bye
= self
.untagged_responses
.get('BYE')
684 raise self
.abort(bye
[-1])
687 def _command(self
, name
, *args
):
689 if self
.state
not in Commands
[name
]:
692 'command %s illegal in state %s' % (name
, self
.state
))
694 for typ
in ('OK', 'NO', 'BAD'):
695 if self
.untagged_responses
.has_key(typ
):
696 del self
.untagged_responses
[typ
]
698 if self
.untagged_responses
.has_key('READ-ONLY') \
699 and not self
.is_readonly
:
700 raise self
.readonly('mailbox status changed to READ-ONLY')
702 tag
= self
._new
_tag
()
703 data
= '%s %s' % (tag
, name
)
705 if arg
is None: continue
706 data
= '%s %s' % (data
, self
._checkquote
(arg
))
708 literal
= self
.literal
709 if literal
is not None:
711 if type(literal
) is type(self
._command
):
715 data
= '%s {%s}' % (data
, len(literal
))
724 self
.send('%s%s' % (data
, CRLF
))
725 except (socket
.error
, OSError), val
:
726 raise self
.abort('socket error: %s' % val
)
732 # Wait for continuation response
734 while self
._get
_response
():
735 if self
.tagged_commands
[tag
]: # BAD/NO?
741 literal
= literator(self
.continuation_response
)
745 _mesg('write literal size %s' % len(literal
))
750 except (socket
.error
, OSError), val
:
751 raise self
.abort('socket error: %s' % val
)
759 def _command_complete(self
, name
, tag
):
762 typ
, data
= self
._get
_tagged
_response
(tag
)
763 except self
.abort
, val
:
764 raise self
.abort('command: %s => %s' % (name
, val
))
765 except self
.error
, val
:
766 raise self
.error('command: %s => %s' % (name
, val
))
769 raise self
.error('%s command error: %s %s' % (name
, typ
, data
))
773 def _get_response(self
):
775 # Read response and store.
777 # Returns None for continuation responses,
778 # otherwise first response line received.
780 resp
= self
._get
_line
()
782 # Command completion response?
784 if self
._match
(self
.tagre
, resp
):
785 tag
= self
.mo
.group('tag')
786 if not self
.tagged_commands
.has_key(tag
):
787 raise self
.abort('unexpected tagged response: %s' % resp
)
789 typ
= self
.mo
.group('type')
790 dat
= self
.mo
.group('data')
791 self
.tagged_commands
[tag
] = (typ
, [dat
])
795 # '*' (untagged) responses?
797 if not self
._match
(Untagged_response
, resp
):
798 if self
._match
(Untagged_status
, resp
):
799 dat2
= self
.mo
.group('data2')
802 # Only other possibility is '+' (continuation) response...
804 if self
._match
(Continuation
, resp
):
805 self
.continuation_response
= self
.mo
.group('data')
806 return None # NB: indicates continuation
808 raise self
.abort("unexpected response: '%s'" % resp
)
810 typ
= self
.mo
.group('type')
811 dat
= self
.mo
.group('data')
812 if dat
is None: dat
= '' # Null untagged response
813 if dat2
: dat
= dat
+ ' ' + dat2
815 # Is there a literal to come?
817 while self
._match
(Literal
, dat
):
819 # Read literal direct from connection.
821 size
= int(self
.mo
.group('size'))
824 _mesg('read literal size %s' % size
)
825 data
= self
.read(size
)
827 # Store response with literal as tuple
829 self
._append
_untagged
(typ
, (dat
, data
))
831 # Read trailer - possibly containing another literal
833 dat
= self
._get
_line
()
835 self
._append
_untagged
(typ
, dat
)
837 # Bracketed response information?
839 if typ
in ('OK', 'NO', 'BAD') and self
._match
(Response_code
, dat
):
840 self
._append
_untagged
(self
.mo
.group('type'), self
.mo
.group('data'))
843 if self
.debug
>= 1 and typ
in ('NO', 'BAD', 'BYE'):
844 _mesg('%s response: %s' % (typ
, dat
))
849 def _get_tagged_response(self
, tag
):
852 result
= self
.tagged_commands
[tag
]
853 if result
is not None:
854 del self
.tagged_commands
[tag
]
857 # Some have reported "unexpected response" exceptions.
858 # Note that ignoring them here causes loops.
859 # Instead, send me details of the unexpected response and
860 # I'll update the code in `_get_response()'.
864 except self
.abort
, val
:
873 line
= self
.readline()
875 raise self
.abort('socket error: EOF')
877 # Protocol mandates all lines terminated by CRLF
888 def _match(self
, cre
, s
):
890 # Run compiled regular expression match method on 's'.
891 # Save result, return success.
893 self
.mo
= cre
.match(s
)
895 if self
.mo
is not None and self
.debug
>= 5:
896 _mesg("\tmatched r'%s' => %s" % (cre
.pattern
, `self
.mo
.groups()`
))
897 return self
.mo
is not None
902 tag
= '%s%s' % (self
.tagpre
, self
.tagnum
)
903 self
.tagnum
= self
.tagnum
+ 1
904 self
.tagged_commands
[tag
] = None
908 def _checkquote(self
, arg
):
910 # Must quote command args if non-alphanumeric chars present,
911 # and not already quoted.
913 if type(arg
) is not type(''):
915 if (arg
[0],arg
[-1]) in (('(',')'),('"','"')):
917 if self
.mustquote
.search(arg
) is None:
919 return self
._quote
(arg
)
922 def _quote(self
, arg
):
924 arg
= arg
.replace('\\', '\\\\')
925 arg
= arg
.replace('"', '\\"')
930 def _simple_command(self
, name
, *args
):
932 return self
._command
_complete
(name
, apply(self
._command
, (name
,) + args
))
935 def _untagged_response(self
, typ
, dat
, name
):
939 if not self
.untagged_responses
.has_key(name
):
941 data
= self
.untagged_responses
[name
]
944 _mesg('untagged_responses[%s] => %s' % (name
, data
))
945 del self
.untagged_responses
[name
]
950 class _Authenticator
:
952 """Private class to provide en/decoding
953 for base64-based authentication conversation.
956 def __init__(self
, mechinst
):
957 self
.mech
= mechinst
# Callable object to provide/process data
959 def process(self
, data
):
960 ret
= self
.mech(self
.decode(data
))
962 return '*' # Abort conversation
963 return self
.encode(ret
)
965 def encode(self
, inp
):
967 # Invoke binascii.b2a_base64 iteratively with
968 # short even length buffers, strip the trailing
969 # line feed from the result and append. "Even"
970 # means a number that factors to both 6 and 8,
971 # so when it gets to the end of the 8-bit input
972 # there's no partial 6-bit output.
982 e
= binascii
.b2a_base64(t
)
987 def decode(self
, inp
):
990 return binascii
.a2b_base64(inp
)
994 Mon2num
= {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
995 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
997 def Internaldate2tuple(resp
):
998 """Convert IMAP4 INTERNALDATE to UT.
1000 Returns Python time module tuple.
1003 mo
= InternalDate
.match(resp
)
1007 mon
= Mon2num
[mo
.group('mon')]
1008 zonen
= mo
.group('zonen')
1010 day
= int(mo
.group('day'))
1011 year
= int(mo
.group('year'))
1012 hour
= int(mo
.group('hour'))
1013 min = int(mo
.group('min'))
1014 sec
= int(mo
.group('sec'))
1015 zoneh
= int(mo
.group('zoneh'))
1016 zonem
= int(mo
.group('zonem'))
1018 # INTERNALDATE timezone must be subtracted to get UT
1020 zone
= (zoneh
*60 + zonem
)*60
1024 tt
= (year
, mon
, day
, hour
, min, sec
, -1, -1, -1)
1026 utc
= time
.mktime(tt
)
1028 # Following is necessary because the time module has no 'mkgmtime'.
1029 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1031 lt
= time
.localtime(utc
)
1032 if time
.daylight
and lt
[-1]:
1033 zone
= zone
+ time
.altzone
1035 zone
= zone
+ time
.timezone
1037 return time
.localtime(utc
- zone
)
1043 """Convert integer to A-P string representation."""
1045 val
= ''; AP
= 'ABCDEFGHIJKLMNOP'
1048 num
, mod
= divmod(num
, 16)
1054 def ParseFlags(resp
):
1056 """Convert IMAP4 flags response to python tuple."""
1058 mo
= Flags
.match(resp
)
1062 return tuple(mo
.group('flags').split())
1065 def Time2Internaldate(date_time
):
1067 """Convert 'date_time' to IMAP4 INTERNALDATE representation.
1069 Return string in form: '"DD-Mmm-YYYY HH:MM:SS +HHMM"'
1072 dttype
= type(date_time
)
1073 if dttype
is type(1) or dttype
is type(1.1):
1074 tt
= time
.localtime(date_time
)
1075 elif dttype
is type(()):
1077 elif dttype
is type(""):
1078 return date_time
# Assume in correct format
1079 else: raise ValueError
1081 dt
= time
.strftime("%d-%b-%Y %H:%M:%S", tt
)
1084 if time
.daylight
and tt
[-1]:
1085 zone
= -time
.altzone
1087 zone
= -time
.timezone
1088 return '"' + dt
+ " %+02d%02d" % divmod(zone
/60, 60) + '"'
1094 def _mesg(s
, secs
=None):
1097 tm
= time
.strftime('%M:%S', time
.localtime(secs
))
1098 sys
.stderr
.write(' %s.%02d %s\n' % (tm
, (secs
*100)%100, s
))
1102 # Dump untagged responses (in `dict').
1106 l
= map(lambda x
:'%s: "%s"' % (x
[0], x
[1][0] and '" "'.join(x
[1]) or ''), l
)
1107 _mesg('untagged responses dump:%s%s' % (t
, t
.join(l
)))
1109 _cmd_log
= [] # Last `_cmd_log_len' interactions
1113 # Keep log of last `_cmd_log_len' interactions for debugging.
1114 if len(_cmd_log
) == _cmd_log_len
:
1116 _cmd_log
.append((time
.time(), line
))
1119 _mesg('last %d IMAP4 interactions:' % len(_cmd_log
))
1120 for secs
,line
in _cmd_log
:
1125 if __name__
== '__main__':
1127 import getopt
, getpass
1130 optlist
, args
= getopt
.getopt(sys
.argv
[1:], 'd:')
1131 except getopt
.error
, val
:
1134 for opt
,val
in optlist
:
1138 if not args
: args
= ('',)
1142 USER
= getpass
.getuser()
1143 PASSWD
= getpass
.getpass("IMAP password for %s on %s: " % (USER
, host
or "localhost"))
1145 test_mesg
= 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)sdata...%(lf)s' % {'user':USER
, 'lf':CRLF
}
1147 ('login', (USER
, PASSWD
)),
1148 ('create', ('/tmp/xxx 1',)),
1149 ('rename', ('/tmp/xxx 1', '/tmp/yyy')),
1150 ('CREATE', ('/tmp/yyz 2',)),
1151 ('append', ('/tmp/yyz 2', None, None, test_mesg
)),
1152 ('list', ('/tmp', 'yy*')),
1153 ('select', ('/tmp/yyz 2',)),
1154 ('search', (None, 'SUBJECT', 'test')),
1155 ('partial', ('1', 'RFC822', 1, 1024)),
1156 ('store', ('1', 'FLAGS', '(\Deleted)')),
1165 ('response',('UIDVALIDITY',)),
1166 ('uid', ('SEARCH', 'ALL')),
1167 ('response', ('EXISTS',)),
1168 ('append', (None, None, None, test_mesg
)),
1174 _mesg('%s %s' % (cmd
, args
))
1175 typ
, dat
= apply(getattr(M
, cmd
), args
)
1176 _mesg('%s => %s %s' % (cmd
, typ
, dat
))
1181 _mesg('PROTOCOL_VERSION = %s' % M
.PROTOCOL_VERSION
)
1182 _mesg('CAPABILITIES = %s' % `M
.capabilities`
)
1184 for cmd
,args
in test_seq1
:
1187 for ml
in run('list', ('/tmp/', 'yy%')):
1188 mo
= re
.match(r
'.*"([^"]+)"$', ml
)
1189 if mo
: path
= mo
.group(1)
1190 else: path
= ml
.split()[-1]
1191 run('delete', (path
,))
1193 for cmd
,args
in test_seq2
:
1194 dat
= run(cmd
, args
)
1196 if (cmd
,args
) != ('uid', ('SEARCH', 'ALL')):
1199 uid
= dat
[-1].split()
1200 if not uid
: continue
1201 run('uid', ('FETCH', '%s' % uid
[-1],
1202 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'))
1204 print '\nAll tests OK.'
1207 print '\nTests failed.'
1211 If you would like to see debugging output,