]> code.delx.au - offlineimap/blob - offlineimap/imaplib2.py
use latest version of imaplib2
[offlineimap] / offlineimap / imaplib2.py
1 #!/usr/bin/env python
2
3 """Threaded IMAP4 client.
4
5 Based on RFC 2060 and original imaplib module.
6
7 Public classes: IMAP4
8 IMAP4_SSL
9 IMAP4_stream
10
11 Public functions: Internaldate2Time
12 ParseFlags
13 Time2Internaldate
14 """
15
16
17 __all__ = ("IMAP4", "IMAP4_SSL", "IMAP4_stream",
18 "Internaldate2Time", "ParseFlags", "Time2Internaldate")
19
20 __version__ = "2.11"
21 __release__ = "2"
22 __revision__ = "11"
23 __credits__ = """
24 Authentication code contributed by Donn Cave <donn@u.washington.edu> June 1998.
25 String method conversion by ESR, February 2001.
26 GET/SETACL contributed by Anthony Baxter <anthony@interlink.com.au> April 2001.
27 IMAP4_SSL contributed by Tino Lange <Tino.Lange@isg.de> March 2002.
28 GET/SETQUOTA contributed by Andreas Zeidler <az@kreativkombinat.de> June 2002.
29 PROXYAUTH contributed by Rick Holbert <holbert.13@osu.edu> November 2002.
30 IDLE via threads suggested by Philippe Normand <phil@respyre.org> January 2005.
31 GET/SETANNOTATION contributed by Tomas Lindroos <skitta@abo.fi> June 2005.
32 COMPRESS/DEFLATE contributed by Bron Gondwana <brong@brong.net> May 2009."""
33 __author__ = "Piers Lauder <piers@janeelix.com>"
34 __URL__ = "http://janeelix.com/piers/python/imaplib2"
35
36 import binascii, os, Queue, random, re, select, socket, sys, time, threading, zlib
37
38 select_module = select
39
40 # Globals
41
42 CRLF = '\r\n'
43 Debug = None # Backward compatibility
44 IMAP4_PORT = 143
45 IMAP4_SSL_PORT = 993
46
47 IDLE_TIMEOUT_RESPONSE = '* IDLE TIMEOUT'
48 IDLE_TIMEOUT = 60*29 # Don't stay in IDLE state longer
49
50 AllowedVersions = ('IMAP4REV1', 'IMAP4') # Most recent first
51
52 # Commands
53
54 CMD_VAL_STATES = 0
55 CMD_VAL_ASYNC = 1
56 NONAUTH, AUTH, SELECTED, LOGOUT = 'NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'
57
58 Commands = {
59 # name valid states asynchronous
60 'APPEND': ((AUTH, SELECTED), False),
61 'AUTHENTICATE': ((NONAUTH,), False),
62 'CAPABILITY': ((NONAUTH, AUTH, SELECTED), True),
63 'CHECK': ((SELECTED,), True),
64 'CLOSE': ((SELECTED,), False),
65 'COMPRESS': ((AUTH,), False),
66 'COPY': ((SELECTED,), True),
67 'CREATE': ((AUTH, SELECTED), True),
68 'DELETE': ((AUTH, SELECTED), True),
69 'DELETEACL': ((AUTH, SELECTED), True),
70 'EXAMINE': ((AUTH, SELECTED), False),
71 'EXPUNGE': ((SELECTED,), True),
72 'FETCH': ((SELECTED,), True),
73 'GETACL': ((AUTH, SELECTED), True),
74 'GETANNOTATION':((AUTH, SELECTED), True),
75 'GETQUOTA': ((AUTH, SELECTED), True),
76 'GETQUOTAROOT': ((AUTH, SELECTED), True),
77 'IDLE': ((SELECTED,), False),
78 'LIST': ((AUTH, SELECTED), True),
79 'LOGIN': ((NONAUTH,), False),
80 'LOGOUT': ((NONAUTH, AUTH, LOGOUT, SELECTED), False),
81 'LSUB': ((AUTH, SELECTED), True),
82 'MYRIGHTS': ((AUTH, SELECTED), True),
83 'NAMESPACE': ((AUTH, SELECTED), True),
84 'NOOP': ((NONAUTH, AUTH, SELECTED), True),
85 'PARTIAL': ((SELECTED,), True),
86 'PROXYAUTH': ((AUTH,), False),
87 'RENAME': ((AUTH, SELECTED), True),
88 'SEARCH': ((SELECTED,), True),
89 'SELECT': ((AUTH, SELECTED), False),
90 'SETACL': ((AUTH, SELECTED), False),
91 'SETANNOTATION':((AUTH, SELECTED), True),
92 'SETQUOTA': ((AUTH, SELECTED), False),
93 'SORT': ((SELECTED,), True),
94 'STATUS': ((AUTH, SELECTED), True),
95 'STORE': ((SELECTED,), True),
96 'SUBSCRIBE': ((AUTH, SELECTED), False),
97 'THREAD': ((SELECTED,), True),
98 'UID': ((SELECTED,), True),
99 'UNSUBSCRIBE': ((AUTH, SELECTED), False),
100 }
101
102 UID_direct = ('SEARCH', 'SORT', 'THREAD')
103
104
105 def Int2AP(num):
106
107 """string = Int2AP(num)
108 Return 'num' converted to a string using characters from the set 'A'..'P'
109 """
110
111 val, a2p = [], 'ABCDEFGHIJKLMNOP'
112 num = int(abs(num))
113 while num:
114 num, mod = divmod(num, 16)
115 val.insert(0, a2p[mod])
116 return ''.join(val)
117
118
119
120 class Request(object):
121
122 """Private class to represent a request awaiting response."""
123
124 def __init__(self, parent, name=None, callback=None, cb_arg=None):
125 self.name = name
126 self.callback = callback # Function called to process result
127 self.callback_arg = cb_arg # Optional arg passed to "callback"
128
129 self.tag = '%s%s' % (parent.tagpre, parent.tagnum)
130 parent.tagnum += 1
131
132 self.ready = threading.Event()
133 self.response = None
134 self.aborted = None
135 self.data = None
136
137
138 def abort(self, typ, val):
139 self.aborted = (typ, val)
140 self.deliver(None)
141
142
143 def get_response(self, exc_fmt=None):
144 self.callback = None
145 self.ready.wait()
146
147 if self.aborted is not None:
148 typ, val = self.aborted
149 if exc_fmt is None:
150 exc_fmt = '%s - %%s' % typ
151 raise typ(exc_fmt % str(val))
152
153 return self.response
154
155
156 def deliver(self, response):
157 if self.callback is not None:
158 self.callback((response, self.callback_arg, self.aborted))
159 return
160
161 self.response = response
162 self.ready.set()
163
164
165
166
167 class IMAP4(object):
168
169 """Threaded IMAP4 client class.
170
171 Instantiate with:
172 IMAP4(host=None, port=None, debug=None, debug_file=None)
173
174 host - host's name (default: localhost);
175 port - port number (default: standard IMAP4 port);
176 debug - debug level (default: 0 - no debug);
177 debug_file - debug stream (default: sys.stderr).
178
179 All IMAP4rev1 commands are supported by methods of the same name.
180
181 Each command returns a tuple: (type, [data, ...]) where 'type'
182 is usually 'OK' or 'NO', and 'data' is either the text from the
183 tagged response, or untagged results from command. Each 'data' is
184 either a string, or a tuple. If a tuple, then the first part is the
185 header of the response, and the second part contains the data (ie:
186 'literal' value).
187
188 Errors raise the exception class <instance>.error("<reason>").
189 IMAP4 server errors raise <instance>.abort("<reason>"), which is
190 a sub-class of 'error'. Mailbox status changes from READ-WRITE to
191 READ-ONLY raise the exception class <instance>.readonly("<reason>"),
192 which is a sub-class of 'abort'.
193
194 "error" exceptions imply a program error.
195 "abort" exceptions imply the connection should be reset, and
196 the command re-tried.
197 "readonly" exceptions imply the command should be re-tried.
198
199 All commands take two optional named arguments:
200 'callback' and 'cb_arg'
201 If 'callback' is provided then the command is asynchronous, so after
202 the command is queued for transmission, the call returns immediately
203 with the tuple (None, None).
204 The result will be posted by invoking "callback" with one arg, a tuple:
205 callback((result, cb_arg, None))
206 or, if there was a problem:
207 callback((None, cb_arg, (exception class, reason)))
208
209 Otherwise the command is synchronous (waits for result). But note
210 that state-changing commands will both block until previous commands
211 have completed, and block subsequent commands until they have finished.
212
213 All (non-callback) arguments to commands are converted to strings,
214 except for AUTHENTICATE, and the last argument to APPEND which is
215 passed as an IMAP4 literal. If necessary (the string contains any
216 non-printing characters or white-space and isn't enclosed with either
217 parentheses or double quotes) each string is quoted. However, the
218 'password' argument to the LOGIN command is always quoted. If you
219 want to avoid having an argument string quoted (eg: the 'flags'
220 argument to STORE) then enclose the string in parentheses (eg:
221 "(\Deleted)").
222
223 There is one instance variable, 'state', that is useful for tracking
224 whether the client needs to login to the server. If it has the
225 value "AUTH" after instantiating the class, then the connection
226 is pre-authenticated (otherwise it will be "NONAUTH"). Selecting a
227 mailbox changes the state to be "SELECTED", closing a mailbox changes
228 back to "AUTH", and once the client has logged out, the state changes
229 to "LOGOUT" and no further commands may be issued.
230
231 Note: to use this module, you must read the RFCs pertaining to the
232 IMAP4 protocol, as the semantics of the arguments to each IMAP4
233 command are left to the invoker, not to mention the results. Also,
234 most IMAP servers implement a sub-set of the commands available here.
235
236 Note also that you must call logout() to shut down threads before
237 discarding an instance.
238 """
239
240 class error(Exception): pass # Logical errors - debug required
241 class abort(error): pass # Service errors - close and retry
242 class readonly(abort): pass # Mailbox status changed to READ-ONLY
243
244
245 continuation_cre = re.compile(r'\+( (?P<data>.*))?')
246 literal_cre = re.compile(r'.*{(?P<size>\d+)}$')
247 mapCRLF_cre = re.compile(r'\r\n|\r|\n')
248 mustquote_cre = re.compile(r"[^\w!#$%&'*+,.:;<=>?^`|~-]")
249 response_code_cre = re.compile(r'\[(?P<type>[A-Z-]+)( (?P<data>[^\]]*))?\]')
250 untagged_response_cre = re.compile(r'\* (?P<type>[A-Z-]+)( (?P<data>.*))?')
251 untagged_status_cre = re.compile(r'\* (?P<data>\d+) (?P<type>[A-Z-]+)( (?P<data2>.*))?')
252
253
254 def __init__(self, host=None, port=None, debug=None, debug_file=None):
255
256 self.state = NONAUTH # IMAP4 protocol state
257 self.literal = None # A literal argument to a command
258 self.tagged_commands = {} # Tagged commands awaiting response
259 self.untagged_responses = {} # {typ: [data, ...], ...}
260 self.is_readonly = False # READ-ONLY desired state
261 self.idle_rqb = None # Server IDLE Request - see _IdleCont
262 self.idle_timeout = None # Must prod server occasionally
263
264 self._expecting_data = 0 # Expecting message data
265 self._accumulated_data = [] # Message data accumulated so far
266 self._literal_expected = None # Message data descriptor
267
268 self.compressor = None # COMPRESS/DEFLATE if not None
269 self.decompressor = None
270
271 # Create unique tag for this session,
272 # and compile tagged response matcher.
273
274 self.tagnum = 0
275 self.tagpre = Int2AP(random.randint(4096, 65535))
276 self.tagre = re.compile(r'(?P<tag>'
277 + self.tagpre
278 + r'\d+) (?P<type>[A-Z]+) (?P<data>.*)')
279
280 if __debug__: self._init_debug(debug, debug_file)
281
282 # Open socket to server.
283
284 self.open(host, port)
285
286 if __debug__:
287 if debug:
288 self._mesg('connected to %s on port %s' % (self.host, self.port))
289
290 # Threading
291
292 self.Terminate = False
293
294 self.state_change_free = threading.Event()
295 self.state_change_pending = threading.Lock()
296 self.commands_lock = threading.Lock()
297
298 self.ouq = Queue.Queue(10)
299 self.inq = Queue.Queue()
300
301 self.wrth = threading.Thread(target=self._writer)
302 self.wrth.start()
303 self.rdth = threading.Thread(target=self._reader)
304 self.rdth.start()
305 self.inth = threading.Thread(target=self._handler)
306 self.inth.start()
307
308 # Get server welcome message,
309 # request and store CAPABILITY response.
310
311 try:
312 self.welcome = self._request_push(tag='continuation').get_response('IMAP4 protocol error: %s')[1]
313
314 if 'PREAUTH' in self.untagged_responses:
315 self.state = AUTH
316 if __debug__: self._log(1, 'state => AUTH')
317 elif 'OK' in self.untagged_responses:
318 if __debug__: self._log(1, 'state => NONAUTH')
319 else:
320 raise self.error(self.welcome)
321
322 typ, dat = self.capability()
323 if dat == [None]:
324 raise self.error('no CAPABILITY response from server')
325 self.capabilities = tuple(dat[-1].upper().split())
326 if __debug__: self._log(3, 'CAPABILITY: %r' % (self.capabilities,))
327
328 for version in AllowedVersions:
329 if not version in self.capabilities:
330 continue
331 self.PROTOCOL_VERSION = version
332 break
333 else:
334 raise self.error('server not IMAP4 compliant')
335 except:
336 self._close_threads()
337 raise
338
339
340 def __getattr__(self, attr):
341 # Allow UPPERCASE variants of IMAP4 command methods.
342 if attr in Commands:
343 return getattr(self, attr.lower())
344 raise AttributeError("Unknown IMAP4 command: '%s'" % attr)
345
346
347
348 # Overridable methods
349
350
351 def open(self, host=None, port=None):
352 """open(host=None, port=None)
353 Setup connection to remote server on "host:port"
354 (default: localhost:standard IMAP4 port).
355 This connection will be used by the routines:
356 read, send, shutdown, socket."""
357
358 self.host = host is not None and host or ''
359 self.port = port is not None and port or IMAP4_PORT
360 self.sock = self.open_socket()
361 self.read_fd = self.sock.fileno()
362
363
364 def open_socket(self):
365 """open_socket()
366 Open socket choosing first address family available."""
367
368 msg = (-1, 'could not open socket')
369 for res in socket.getaddrinfo(self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM):
370 af, socktype, proto, canonname, sa = res
371 try:
372 s = socket.socket(af, socktype, proto)
373 except socket.error, msg:
374 continue
375 try:
376 s.connect(sa)
377 except socket.error, msg:
378 s.close()
379 continue
380 break
381 else:
382 raise socket.error(msg)
383
384 return s
385
386
387 def start_compressing(self):
388 """start_compressing()
389 Enable deflate compression on the socket (RFC 4978)."""
390
391 # rfc 1951 - pure DEFLATE, so use -15 for both windows
392 self.decompressor = zlib.decompressobj(-15)
393 self.compressor = zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, -15)
394
395
396 def read(self, size):
397 """data = read(size)
398 Read at most 'size' bytes from remote."""
399
400 if self.decompressor is None:
401 return self.sock.recv(size)
402
403 if self.decompressor.unconsumed_tail:
404 data = self.decompressor.unconsumed_tail
405 else:
406 data = self.sock.recv(8192)
407
408 return self.decompressor.decompress(data, size)
409
410
411 def send(self, data):
412 """send(data)
413 Send 'data' to remote."""
414
415 if self.compressor is not None:
416 data = self.compressor.compress(data)
417 data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
418
419 self.sock.sendall(data)
420
421
422 def shutdown(self):
423 """shutdown()
424 Close I/O established in "open"."""
425
426 self.sock.close()
427
428
429 def socket(self):
430 """socket = socket()
431 Return socket instance used to connect to IMAP4 server."""
432
433 return self.sock
434
435
436
437 # Utility methods
438
439
440 def enable_compression(self):
441 """enable_compression()
442 Ask the server to start compressing the connection.
443 Should be called from user of this class after instantiation, as in:
444 if 'COMPRESS=DEFLATE' in imapobj.capabilities:
445 imapobj.enable_compression()"""
446
447 try:
448 typ, dat = self._simple_command('COMPRESS', 'DEFLATE')
449 if typ == 'OK':
450 self.start_compressing()
451 if __debug__: self._log(1, 'Enabled COMPRESS=DEFLATE')
452 finally:
453 self.state_change_pending.release()
454
455
456 def recent(self, **kw):
457 """(typ, [data]) = recent()
458 Return most recent 'RECENT' responses if any exist,
459 else prompt server for an update using the 'NOOP' command.
460 'data' is None if no new messages,
461 else list of RECENT responses, most recent last."""
462
463 name = 'RECENT'
464 typ, dat = self._untagged_response('OK', [None], name)
465 if dat[-1]:
466 return self._deliver_dat(typ, dat, kw)
467 kw['untagged_response'] = name
468 return self.noop(**kw) # Prod server for response
469
470
471 def response(self, code, **kw):
472 """(code, [data]) = response(code)
473 Return data for response 'code' if received, or None.
474 Old value for response 'code' is cleared."""
475
476 typ, dat = self._untagged_response(code, [None], code.upper())
477 return self._deliver_dat(typ, dat, kw)
478
479
480
481
482 # IMAP4 commands
483
484
485 def append(self, mailbox, flags, date_time, message, **kw):
486 """(typ, [data]) = append(mailbox, flags, date_time, message)
487 Append message to named mailbox.
488 All args except `message' can be None."""
489
490 name = 'APPEND'
491 if not mailbox:
492 mailbox = 'INBOX'
493 if flags:
494 if (flags[0],flags[-1]) != ('(',')'):
495 flags = '(%s)' % flags
496 else:
497 flags = None
498 if date_time:
499 date_time = Time2Internaldate(date_time)
500 else:
501 date_time = None
502 self.literal = self.mapCRLF_cre.sub(CRLF, message)
503 try:
504 return self._simple_command(name, mailbox, flags, date_time, **kw)
505 finally:
506 self.state_change_pending.release()
507
508
509 def authenticate(self, mechanism, authobject, **kw):
510 """(typ, [data]) = authenticate(mechanism, authobject)
511 Authenticate command - requires response processing.
512
513 'mechanism' specifies which authentication mechanism is to
514 be used - it must appear in <instance>.capabilities in the
515 form AUTH=<mechanism>.
516
517 'authobject' must be a callable object:
518
519 data = authobject(response)
520
521 It will be called to process server continuation responses.
522 It should return data that will be encoded and sent to server.
523 It should return None if the client abort response '*' should
524 be sent instead."""
525
526 self.literal = _Authenticator(authobject).process
527 try:
528 typ, dat = self._simple_command('AUTHENTICATE', mechanism.upper())
529 if typ != 'OK':
530 self._deliver_exc(self.error, dat[-1])
531 self.state = AUTH
532 if __debug__: self._log(1, 'state => AUTH')
533 finally:
534 self.state_change_pending.release()
535 return self._deliver_dat(typ, dat, kw)
536
537
538 def capability(self, **kw):
539 """(typ, [data]) = capability()
540 Fetch capabilities list from server."""
541
542 name = 'CAPABILITY'
543 kw['untagged_response'] = name
544 return self._simple_command(name, **kw)
545
546
547 def check(self, **kw):
548 """(typ, [data]) = check()
549 Checkpoint mailbox on server."""
550
551 return self._simple_command('CHECK', **kw)
552
553
554 def close(self, **kw):
555 """(typ, [data]) = close()
556 Close currently selected mailbox.
557
558 Deleted messages are removed from writable mailbox.
559 This is the recommended command before 'LOGOUT'."""
560
561 if self.state != 'SELECTED':
562 raise self.error('No mailbox selected.')
563 try:
564 typ, dat = self._simple_command('CLOSE')
565 finally:
566 self.state = AUTH
567 if __debug__: self._log(1, 'state => AUTH')
568 self.state_change_pending.release()
569 return self._deliver_dat(typ, dat, kw)
570
571
572 def copy(self, message_set, new_mailbox, **kw):
573 """(typ, [data]) = copy(message_set, new_mailbox)
574 Copy 'message_set' messages onto end of 'new_mailbox'."""
575
576 return self._simple_command('COPY', message_set, new_mailbox, **kw)
577
578
579 def create(self, mailbox, **kw):
580 """(typ, [data]) = create(mailbox)
581 Create new mailbox."""
582
583 return self._simple_command('CREATE', mailbox, **kw)
584
585
586 def delete(self, mailbox, **kw):
587 """(typ, [data]) = delete(mailbox)
588 Delete old mailbox."""
589
590 return self._simple_command('DELETE', mailbox, **kw)
591
592
593 def deleteacl(self, mailbox, who, **kw):
594 """(typ, [data]) = deleteacl(mailbox, who)
595 Delete the ACLs (remove any rights) set for who on mailbox."""
596
597 return self._simple_command('DELETEACL', mailbox, who, **kw)
598
599
600 def examine(self, mailbox='INBOX', **kw):
601 """(typ, [data]) = examine(mailbox='INBOX', readonly=False)
602 Select a mailbox for READ-ONLY access. (Flushes all untagged responses.)
603 'data' is count of messages in mailbox ('EXISTS' response).
604 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
605 other responses should be obtained via "response('FLAGS')" etc."""
606
607 return self.select(mailbox=mailbox, readonly=True, **kw)
608
609
610 def expunge(self, **kw):
611 """(typ, [data]) = expunge()
612 Permanently remove deleted items from selected mailbox.
613 Generates 'EXPUNGE' response for each deleted message.
614 'data' is list of 'EXPUNGE'd message numbers in order received."""
615
616 name = 'EXPUNGE'
617 kw['untagged_response'] = name
618 return self._simple_command(name, **kw)
619
620
621 def fetch(self, message_set, message_parts, **kw):
622 """(typ, [data, ...]) = fetch(message_set, message_parts)
623 Fetch (parts of) messages.
624 'message_parts' should be a string of selected parts
625 enclosed in parentheses, eg: "(UID BODY[TEXT])".
626 'data' are tuples of message part envelope and data,
627 followed by a string containing the trailer."""
628
629 name = 'FETCH'
630 kw['untagged_response'] = name
631 return self._simple_command(name, message_set, message_parts, **kw)
632
633
634 def getacl(self, mailbox, **kw):
635 """(typ, [data]) = getacl(mailbox)
636 Get the ACLs for a mailbox."""
637
638 kw['untagged_response'] = 'ACL'
639 return self._simple_command('GETACL', mailbox, **kw)
640
641
642 def getannotation(self, mailbox, entry, attribute, **kw):
643 """(typ, [data]) = getannotation(mailbox, entry, attribute)
644 Retrieve ANNOTATIONs."""
645
646 kw['untagged_response'] = 'ANNOTATION'
647 return self._simple_command('GETANNOTATION', mailbox, entry, attribute, **kw)
648
649
650 def getquota(self, root, **kw):
651 """(typ, [data]) = getquota(root)
652 Get the quota root's resource usage and limits.
653 (Part of the IMAP4 QUOTA extension defined in rfc2087.)"""
654
655 kw['untagged_response'] = 'QUOTA'
656 return self._simple_command('GETQUOTA', root, **kw)
657
658
659 def getquotaroot(self, mailbox, **kw):
660 # Hmmm, this is non-std! Left for backwards-compatibility, sigh.
661 # NB: usage should have been defined as:
662 # (typ, [QUOTAROOT responses...]) = getquotaroot(mailbox)
663 # (typ, [QUOTA responses...]) = response('QUOTA')
664 """(typ, [[QUOTAROOT responses...], [QUOTA responses...]]) = getquotaroot(mailbox)
665 Get the list of quota roots for the named mailbox."""
666
667 typ, dat = self._simple_command('GETQUOTAROOT', mailbox)
668 typ, quota = self._untagged_response(typ, dat, 'QUOTA')
669 typ, quotaroot = self._untagged_response(typ, dat, 'QUOTAROOT')
670 return self._deliver_dat(typ, [quotaroot, quota], kw)
671
672
673 def idle(self, timeout=None, **kw):
674 """"(typ, [data]) = idle(timeout=None)
675 Put server into IDLE mode until server notifies some change,
676 or 'timeout' (secs) occurs (default: 29 minutes),
677 or another IMAP4 command is scheduled."""
678
679 name = 'IDLE'
680 self.literal = _IdleCont(self, timeout).process
681 try:
682 return self._simple_command(name, **kw)
683 finally:
684 self.state_change_pending.release()
685
686
687 def list(self, directory='""', pattern='*', **kw):
688 """(typ, [data]) = list(directory='""', pattern='*')
689 List mailbox names in directory matching pattern.
690 'data' is list of LIST responses.
691
692 NB: for 'pattern':
693 % matches all except separator ( so LIST "" "%" returns names at root)
694 * matches all (so LIST "" "*" returns whole directory tree from root)"""
695
696 name = 'LIST'
697 kw['untagged_response'] = name
698 return self._simple_command(name, directory, pattern, **kw)
699
700
701 def login(self, user, password, **kw):
702 """(typ, [data]) = login(user, password)
703 Identify client using plaintext password.
704 NB: 'password' will be quoted."""
705
706 try:
707 typ, dat = self._simple_command('LOGIN', user, self._quote(password))
708 if typ != 'OK':
709 self._deliver_exc(self.error, dat[-1], kw)
710 self.state = AUTH
711 if __debug__: self._log(1, 'state => AUTH')
712 finally:
713 self.state_change_pending.release()
714 return self._deliver_dat(typ, dat, kw)
715
716
717 def login_cram_md5(self, user, password, **kw):
718 """(typ, [data]) = login_cram_md5(user, password)
719 Force use of CRAM-MD5 authentication."""
720
721 self.user, self.password = user, password
722 return self.authenticate('CRAM-MD5', self._CRAM_MD5_AUTH, **kw)
723
724
725 def _CRAM_MD5_AUTH(self, challenge):
726 """Authobject to use with CRAM-MD5 authentication."""
727 import hmac
728 return self.user + " " + hmac.HMAC(self.password, challenge).hexdigest()
729
730
731 def logout(self, **kw):
732 """(typ, [data]) = logout()
733 Shutdown connection to server.
734 Returns server 'BYE' response."""
735
736 self.state = LOGOUT
737 if __debug__: self._log(1, 'state => LOGOUT')
738
739 try:
740 typ, dat = self._simple_command('LOGOUT')
741 except:
742 typ, dat = 'NO', ['%s: %s' % sys.exc_info()[:2]]
743 if __debug__: self._log(1, dat)
744
745 self._close_threads()
746
747 self.state_change_pending.release()
748
749 if __debug__: self._log(1, 'connection closed')
750
751 bye = self.untagged_responses.get('BYE')
752 if bye:
753 typ, dat = 'BYE', bye
754 return self._deliver_dat(typ, dat, kw)
755
756
757 def lsub(self, directory='""', pattern='*', **kw):
758 """(typ, [data, ...]) = lsub(directory='""', pattern='*')
759 List 'subscribed' mailbox names in directory matching pattern.
760 'data' are tuples of message part envelope and data."""
761
762 name = 'LSUB'
763 kw['untagged_response'] = name
764 return self._simple_command(name, directory, pattern, **kw)
765
766
767 def myrights(self, mailbox):
768 """(typ, [data]) = myrights(mailbox)
769 Show my ACLs for a mailbox (i.e. the rights that I have on mailbox)."""
770
771 name = 'MYRIGHTS'
772 kw['untagged_response'] = name
773 return self._simple_command(name, mailbox, **kw)
774
775
776 def namespace(self, **kw):
777 """(typ, [data, ...]) = namespace()
778 Returns IMAP namespaces ala rfc2342."""
779
780 name = 'NAMESPACE'
781 kw['untagged_response'] = name
782 return self._simple_command(name, **kw)
783
784
785 def noop(self, **kw):
786 """(typ, [data]) = noop()
787 Send NOOP command."""
788
789 if __debug__: self._dump_ur(3)
790 return self._simple_command('NOOP', **kw)
791
792
793 def partial(self, message_num, message_part, start, length, **kw):
794 """(typ, [data, ...]) = partial(message_num, message_part, start, length)
795 Fetch truncated part of a message.
796 'data' is tuple of message part envelope and data.
797 NB: obsolete."""
798
799 name = 'PARTIAL'
800 kw['untagged_response'] = 'FETCH'
801 return self._simple_command(name, message_num, message_part, start, length, **kw)
802
803
804 def proxyauth(self, user, **kw):
805 """(typ, [data]) = proxyauth(user)
806 Assume authentication as 'user'.
807 (Allows an authorised administrator to proxy into any user's mailbox.)"""
808
809 try:
810 return self._simple_command('PROXYAUTH', user, **kw)
811 finally:
812 self.state_change_pending.release()
813
814
815 def rename(self, oldmailbox, newmailbox, **kw):
816 """(typ, [data]) = rename(oldmailbox, newmailbox)
817 Rename old mailbox name to new."""
818
819 return self._simple_command('RENAME', oldmailbox, newmailbox, **kw)
820
821
822 def search(self, charset, *criteria, **kw):
823 """(typ, [data]) = search(charset, criterion, ...)
824 Search mailbox for matching messages.
825 'data' is space separated list of matching message numbers."""
826
827 name = 'SEARCH'
828 kw['untagged_response'] = name
829 if charset:
830 return self._simple_command(name, 'CHARSET', charset, *criteria, **kw)
831 return self._simple_command(name, *criteria, **kw)
832
833
834 def select(self, mailbox='INBOX', readonly=False, **kw):
835 """(typ, [data]) = select(mailbox='INBOX', readonly=False)
836 Select a mailbox. (Flushes all untagged responses.)
837 'data' is count of messages in mailbox ('EXISTS' response).
838 Mandated responses are ('FLAGS', 'EXISTS', 'RECENT', 'UIDVALIDITY'), so
839 other responses should be obtained via "response('FLAGS')" etc."""
840
841 self.commands_lock.acquire()
842 self.untagged_responses = {} # Flush old responses.
843 self.commands_lock.release()
844
845 self.is_readonly = readonly and True or False
846 if readonly:
847 name = 'EXAMINE'
848 else:
849 name = 'SELECT'
850 try:
851 rqb = self._command(name, mailbox)
852 typ, dat = rqb.get_response('command: %s => %%s' % rqb.name)
853 if typ != 'OK':
854 if self.state == SELECTED:
855 self.state = AUTH
856 if __debug__: self._log(1, 'state => AUTH')
857 if typ == 'BAD':
858 self._deliver_exc(self.error, '%s command error: %s %s. Data: %.100s' % (name, typ, dat, mailbox), kw)
859 return self._deliver_dat(typ, dat, kw)
860 self.state = SELECTED
861 if __debug__: self._log(1, 'state => SELECTED')
862 finally:
863 self.state_change_pending.release()
864 if 'READ-ONLY' in self.untagged_responses and not readonly:
865 if __debug__: self._dump_ur(1)
866 self._deliver_exc(self.readonly, '%s is not writable' % mailbox, kw)
867 return self._deliver_dat(typ, self.untagged_responses.get('EXISTS', [None]), kw)
868
869
870 def setacl(self, mailbox, who, what, **kw):
871 """(typ, [data]) = setacl(mailbox, who, what)
872 Set a mailbox acl."""
873
874 try:
875 return self._simple_command('SETACL', mailbox, who, what, **kw)
876 finally:
877 self.state_change_pending.release()
878
879
880 def setannotation(self, *args, **kw):
881 """(typ, [data]) = setannotation(mailbox[, entry, attribute]+)
882 Set ANNOTATIONs."""
883
884 kw['untagged_response'] = 'ANNOTATION'
885 return self._simple_command('SETANNOTATION', *args, **kw)
886
887
888 def setquota(self, root, limits, **kw):
889 """(typ, [data]) = setquota(root, limits)
890 Set the quota root's resource limits."""
891
892 kw['untagged_response'] = 'QUOTA'
893 try:
894 return self._simple_command('SETQUOTA', root, limits, **kw)
895 finally:
896 self.state_change_pending.release()
897
898
899 def sort(self, sort_criteria, charset, *search_criteria, **kw):
900 """(typ, [data]) = sort(sort_criteria, charset, search_criteria, ...)
901 IMAP4rev1 extension SORT command."""
902
903 name = 'SORT'
904 if (sort_criteria[0],sort_criteria[-1]) != ('(',')'):
905 sort_criteria = '(%s)' % sort_criteria
906 kw['untagged_response'] = name
907 return self._simple_command(name, sort_criteria, charset, *search_criteria, **kw)
908
909
910 def status(self, mailbox, names, **kw):
911 """(typ, [data]) = status(mailbox, names)
912 Request named status conditions for mailbox."""
913
914 name = 'STATUS'
915 kw['untagged_response'] = name
916 return self._simple_command(name, mailbox, names, **kw)
917
918
919 def store(self, message_set, command, flags, **kw):
920 """(typ, [data]) = store(message_set, command, flags)
921 Alters flag dispositions for messages in mailbox."""
922
923 if (flags[0],flags[-1]) != ('(',')'):
924 flags = '(%s)' % flags # Avoid quoting the flags
925 kw['untagged_response'] = 'FETCH'
926 return self._simple_command('STORE', message_set, command, flags, **kw)
927
928
929 def subscribe(self, mailbox, **kw):
930 """(typ, [data]) = subscribe(mailbox)
931 Subscribe to new mailbox."""
932
933 try:
934 return self._simple_command('SUBSCRIBE', mailbox, **kw)
935 finally:
936 self.state_change_pending.release()
937
938
939 def thread(self, threading_algorithm, charset, *search_criteria, **kw):
940 """(type, [data]) = thread(threading_alogrithm, charset, search_criteria, ...)
941 IMAPrev1 extension THREAD command."""
942
943 name = 'THREAD'
944 kw['untagged_response'] = name
945 return self._simple_command(name, threading_algorithm, charset, *search_criteria, **kw)
946
947
948 def uid(self, command, *args, **kw):
949 """(typ, [data]) = uid(command, arg, ...)
950 Execute "command arg ..." with messages identified by UID,
951 rather than message number.
952 Assumes 'command' is legal in current state.
953 Returns response appropriate to 'command'."""
954
955 command = command.upper()
956 if command in UID_direct:
957 resp = command
958 else:
959 resp = 'FETCH'
960 kw['untagged_response'] = resp
961 return self._simple_command('UID', command, *args, **kw)
962
963
964 def unsubscribe(self, mailbox, **kw):
965 """(typ, [data]) = unsubscribe(mailbox)
966 Unsubscribe from old mailbox."""
967
968 try:
969 return self._simple_command('UNSUBSCRIBE', mailbox, **kw)
970 finally:
971 self.state_change_pending.release()
972
973
974 def xatom(self, name, *args, **kw):
975 """(typ, [data]) = xatom(name, arg, ...)
976 Allow simple extension commands notified by server in CAPABILITY response.
977 Assumes extension command 'name' is legal in current state.
978 Returns response appropriate to extension command 'name'."""
979
980 name = name.upper()
981 if not name in Commands:
982 Commands[name] = ((self.state,), False)
983 try:
984 return self._simple_command(name, *args, **kw)
985 finally:
986 if self.state_change_pending.locked():
987 self.state_change_pending.release()
988
989
990
991 # Internal methods
992
993
994 def _append_untagged(self, typ, dat):
995
996 if dat is None: dat = ''
997
998 self.commands_lock.acquire()
999 ur = self.untagged_responses.setdefault(typ, [])
1000 ur.append(dat)
1001 self.commands_lock.release()
1002
1003 if __debug__: self._log(5, 'untagged_responses[%s] %s += ["%s"]' % (typ, len(ur)-1, dat))
1004
1005
1006 def _check_bye(self):
1007
1008 bye = self.untagged_responses.get('BYE')
1009 if bye:
1010 raise self.abort(bye[-1])
1011
1012
1013 def _checkquote(self, arg):
1014
1015 # Must quote command args if non-alphanumeric chars present,
1016 # and not already quoted.
1017
1018 if not isinstance(arg, basestring):
1019 return arg
1020 if len(arg) >= 2 and (arg[0],arg[-1]) in (('(',')'),('"','"')):
1021 return arg
1022 if arg and self.mustquote_cre.search(arg) is None:
1023 return arg
1024 return self._quote(arg)
1025
1026
1027 def _command(self, name, *args, **kw):
1028
1029 if Commands[name][CMD_VAL_ASYNC]:
1030 cmdtyp = 'async'
1031 else:
1032 cmdtyp = 'sync'
1033
1034 if __debug__: self._log(1, '[%s] %s %s' % (cmdtyp, name, args))
1035
1036 self.state_change_pending.acquire()
1037
1038 self._end_idle()
1039
1040 if cmdtyp == 'async':
1041 self.state_change_pending.release()
1042 else:
1043 # Need to wait for all async commands to complete
1044 self._check_bye()
1045 self.commands_lock.acquire()
1046 if self.tagged_commands:
1047 self.state_change_free.clear()
1048 need_event = True
1049 else:
1050 need_event = False
1051 self.commands_lock.release()
1052 if need_event:
1053 if __debug__: self._log(4, 'sync command %s waiting for empty commands Q' % name)
1054 self.state_change_free.wait()
1055 if __debug__: self._log(4, 'sync command %s proceeding' % name)
1056
1057 if self.state not in Commands[name][CMD_VAL_STATES]:
1058 self.literal = None
1059 raise self.error('command %s illegal in state %s'
1060 % (name, self.state))
1061
1062 self._check_bye()
1063
1064 self.commands_lock.acquire()
1065 for typ in ('OK', 'NO', 'BAD'):
1066 if typ in self.untagged_responses:
1067 del self.untagged_responses[typ]
1068 self.commands_lock.release()
1069
1070 if 'READ-ONLY' in self.untagged_responses \
1071 and not self.is_readonly:
1072 self.literal = None
1073 raise self.readonly('mailbox status changed to READ-ONLY')
1074
1075 if self.Terminate:
1076 raise self.abort('connection closed')
1077
1078 rqb = self._request_push(name=name, **kw)
1079
1080 data = '%s %s' % (rqb.tag, name)
1081 for arg in args:
1082 if arg is None: continue
1083 data = '%s %s' % (data, self._checkquote(arg))
1084
1085 literal = self.literal
1086 if literal is not None:
1087 self.literal = None
1088 if isinstance(literal, basestring):
1089 literator = None
1090 data = '%s {%s}' % (data, len(literal))
1091 else:
1092 literator = literal
1093
1094 if __debug__: self._log(4, 'data=%s' % data)
1095
1096 rqb.data = '%s%s' % (data, CRLF)
1097
1098 if literal is None:
1099 self.ouq.put(rqb)
1100 return rqb
1101
1102 # Must setup continuation expectancy *before* ouq.put
1103 crqb = self._request_push(tag='continuation')
1104
1105 self.ouq.put(rqb)
1106
1107 while True:
1108 # Wait for continuation response
1109
1110 ok, data = crqb.get_response('command: %s => %%s' % name)
1111 if __debug__: self._log(3, 'continuation => %s, %s' % (ok, data))
1112
1113 # NO/BAD response?
1114
1115 if not ok:
1116 break
1117
1118 # Send literal
1119
1120 if literator is not None:
1121 literal = literator(data, rqb)
1122
1123 if literal is None:
1124 break
1125
1126 if literator is not None:
1127 # Need new request for next continuation response
1128 crqb = self._request_push(tag='continuation')
1129
1130 if __debug__: self._log(4, 'write literal size %s' % len(literal))
1131 crqb.data = '%s%s' % (literal, CRLF)
1132 self.ouq.put(crqb)
1133
1134 if literator is None:
1135 break
1136
1137 return rqb
1138
1139
1140 def _command_complete(self, rqb, kw):
1141
1142 # Called for non-callback commands
1143
1144 typ, dat = rqb.get_response('command: %s => %%s' % rqb.name)
1145 self._check_bye()
1146 if typ == 'BAD':
1147 if __debug__: self._print_log()
1148 raise self.error('%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data))
1149 if 'untagged_response' in kw:
1150 return self._untagged_response(typ, dat, kw['untagged_response'])
1151 return typ, dat
1152
1153
1154 def _command_completer(self, (response, cb_arg, error)):
1155
1156 # Called for callback commands
1157 rqb, kw = cb_arg
1158 rqb.callback = kw['callback']
1159 rqb.callback_arg = kw.get('cb_arg')
1160 if error is not None:
1161 if __debug__: self._print_log()
1162 typ, val = error
1163 rqb.abort(typ, val)
1164 return
1165 bye = self.untagged_responses.get('BYE')
1166 if bye:
1167 rqb.abort(self.abort, bye[-1])
1168 return
1169 typ, dat = response
1170 if typ == 'BAD':
1171 if __debug__: self._print_log()
1172 rqb.abort(self.error, '%s command error: %s %s. Data: %.100s' % (rqb.name, typ, dat, rqb.data))
1173 return
1174 if 'untagged_response' in kw:
1175 response = self._untagged_response(typ, dat, kw['untagged_response'])
1176 rqb.deliver(response)
1177
1178
1179 def _deliver_dat(self, typ, dat, kw):
1180
1181 if 'callback' in kw:
1182 kw['callback'](((typ, dat), kw.get('cb_arg'), None))
1183 return typ, dat
1184
1185
1186 def _deliver_exc(self, exc, dat, kw):
1187
1188 if 'callback' in kw:
1189 kw['callback']((None, kw.get('cb_arg'), (exc, dat)))
1190 raise exc(dat)
1191
1192
1193 def _end_idle(self):
1194
1195 irqb = self.idle_rqb
1196 if irqb is None:
1197 return
1198 self.idle_rqb = None
1199 self.idle_timeout = None
1200 irqb.data = 'DONE%s' % CRLF
1201 self.ouq.put(irqb)
1202 if __debug__: self._log(2, 'server IDLE finished')
1203
1204
1205 def _match(self, cre, s):
1206
1207 # Run compiled regular expression 'cre' match method on 's'.
1208 # Save result, return success.
1209
1210 self.mo = cre.match(s)
1211 return self.mo is not None
1212
1213
1214 def _put_response(self, resp):
1215
1216 if self._expecting_data > 0:
1217 rlen = len(resp)
1218 dlen = min(self._expecting_data, rlen)
1219 self._expecting_data -= dlen
1220 if rlen <= dlen:
1221 self._accumulated_data.append(resp)
1222 return
1223 self._accumulated_data.append(resp[:dlen])
1224 resp = resp[dlen:]
1225
1226 if self._accumulated_data:
1227 typ, dat = self._literal_expected
1228 self._append_untagged(typ, (dat, ''.join(self._accumulated_data)))
1229 self._accumulated_data = []
1230
1231 # Protocol mandates all lines terminated by CRLF
1232 resp = resp[:-2]
1233
1234 if 'continuation' in self.tagged_commands:
1235 continuation_expected = True
1236 else:
1237 continuation_expected = False
1238
1239 if self._literal_expected is not None:
1240 dat = resp
1241 if self._match(self.literal_cre, dat):
1242 self._literal_expected[1] = dat
1243 self._expecting_data = int(self.mo.group('size'))
1244 if __debug__: self._log(4, 'expecting literal size %s' % self._expecting_data)
1245 return
1246 typ = self._literal_expected[0]
1247 self._literal_expected = None
1248 self._append_untagged(typ, dat) # Tail
1249 if __debug__: self._log(4, 'literal completed')
1250 else:
1251 # Command completion response?
1252 if self._match(self.tagre, resp):
1253 tag = self.mo.group('tag')
1254 typ = self.mo.group('type')
1255 dat = self.mo.group('data')
1256 if not tag in self.tagged_commands:
1257 if __debug__: self._log(1, 'unexpected tagged response: %s' % resp)
1258 else:
1259 self._request_pop(tag, (typ, [dat]))
1260 else:
1261 dat2 = None
1262
1263 # '*' (untagged) responses?
1264
1265 if not self._match(self.untagged_response_cre, resp):
1266 if self._match(self.untagged_status_cre, resp):
1267 dat2 = self.mo.group('data2')
1268
1269 if self.mo is None:
1270 # Only other possibility is '+' (continuation) response...
1271
1272 if self._match(self.continuation_cre, resp):
1273 if not continuation_expected:
1274 if __debug__: self._log(1, "unexpected continuation response: '%s'" % resp)
1275 return
1276 self._request_pop('continuation', (True, self.mo.group('data')))
1277 return
1278
1279 if __debug__: self._log(1, "unexpected response: '%s'" % resp)
1280 return
1281
1282 typ = self.mo.group('type')
1283 dat = self.mo.group('data')
1284 if dat is None: dat = '' # Null untagged response
1285 if dat2: dat = dat + ' ' + dat2
1286
1287 # Is there a literal to come?
1288
1289 if self._match(self.literal_cre, dat):
1290 self._expecting_data = int(self.mo.group('size'))
1291 if __debug__: self._log(4, 'read literal size %s' % self._expecting_data)
1292 self._literal_expected = [typ, dat]
1293 return
1294
1295 self._append_untagged(typ, dat)
1296
1297 if typ != 'OK':
1298 self._end_idle()
1299
1300 # Bracketed response information?
1301
1302 if typ in ('OK', 'NO', 'BAD') and self._match(self.response_code_cre, dat):
1303 self._append_untagged(self.mo.group('type'), self.mo.group('data'))
1304
1305 # Command waiting for aborted continuation response?
1306
1307 if continuation_expected:
1308 self._request_pop('continuation', (False, resp))
1309
1310 # Bad news?
1311
1312 if typ in ('NO', 'BAD', 'BYE'):
1313 if typ == 'BYE':
1314 self.Terminate = True
1315 if __debug__: self._log(1, '%s response: %s' % (typ, dat))
1316
1317
1318 def _quote(self, arg):
1319
1320 return '"%s"' % arg.replace('\\', '\\\\').replace('"', '\\"')
1321
1322
1323 def _request_pop(self, name, data):
1324
1325 if __debug__: self._log(4, '_request_pop(%s, %s)' % (name, data))
1326 self.commands_lock.acquire()
1327 rqb = self.tagged_commands.pop(name)
1328 if not self.tagged_commands:
1329 self.state_change_free.set()
1330 self.commands_lock.release()
1331 rqb.deliver(data)
1332
1333
1334 def _request_push(self, tag=None, name=None, **kw):
1335
1336 self.commands_lock.acquire()
1337 rqb = Request(self, name=name, **kw)
1338 if tag is None:
1339 tag = rqb.tag
1340 self.tagged_commands[tag] = rqb
1341 self.commands_lock.release()
1342 if __debug__: self._log(4, '_request_push(%s, %s, %s)' % (tag, name, `kw`))
1343 return rqb
1344
1345
1346 def _simple_command(self, name, *args, **kw):
1347
1348 if 'callback' in kw:
1349 rqb = self._command(name, callback=self._command_completer, *args)
1350 rqb.callback_arg = (rqb, kw)
1351 return (None, None)
1352 return self._command_complete(self._command(name, *args), kw)
1353
1354
1355 def _untagged_response(self, typ, dat, name):
1356
1357 if typ == 'NO':
1358 return typ, dat
1359 if not name in self.untagged_responses:
1360 return typ, [None]
1361 self.commands_lock.acquire()
1362 data = self.untagged_responses.pop(name)
1363 self.commands_lock.release()
1364 if __debug__: self._log(5, 'pop untagged_responses[%s] => %s' % (name, (typ, data)))
1365 return typ, data
1366
1367
1368
1369 # Threads
1370
1371
1372 def _close_threads(self):
1373
1374 self.ouq.put(None)
1375 self.wrth.join()
1376
1377 self.shutdown()
1378
1379 self.rdth.join()
1380 self.inth.join()
1381
1382
1383 def _handler(self):
1384
1385 threading.currentThread().setName('hdlr')
1386
1387 time.sleep(0.1) # Don't start handling before main thread ready
1388
1389 if __debug__: self._log(1, 'starting')
1390
1391 typ, val = self.abort, 'connection terminated'
1392
1393 while not self.Terminate:
1394 try:
1395 if self.idle_timeout is not None:
1396 timeout = self.idle_timeout - time.time()
1397 if timeout <= 0:
1398 timeout = 1
1399 if __debug__:
1400 if self.idle_rqb is not None:
1401 self._log(5, 'server IDLING, timeout=%.2f' % timeout)
1402 else:
1403 timeout = None
1404 line = self.inq.get(True, timeout)
1405 except Queue.Empty:
1406 if self.idle_rqb is None:
1407 continue
1408 if self.idle_timeout > time.time():
1409 continue
1410 if __debug__: self._log(2, 'server IDLE timedout')
1411 line = IDLE_TIMEOUT_RESPONSE
1412
1413 if line is None:
1414 break
1415
1416 if not isinstance(line, basestring):
1417 typ, val = line
1418 break
1419
1420 try:
1421 self._put_response(line)
1422 except:
1423 typ, val = self.error, 'program error: %s - %s' % sys.exc_info()[:2]
1424 break
1425
1426 self.Terminate = True
1427
1428 while not self.ouq.empty():
1429 try:
1430 self.ouq.get_nowait().abort(typ, val)
1431 except Queue.Empty:
1432 break
1433 self.ouq.put(None)
1434
1435 self.commands_lock.acquire()
1436 for name in self.tagged_commands.keys():
1437 rqb = self.tagged_commands.pop(name)
1438 rqb.abort(typ, val)
1439 self.state_change_free.set()
1440 self.commands_lock.release()
1441
1442 if __debug__: self._log(1, 'finished')
1443
1444
1445 if hasattr(select_module, "poll"):
1446
1447 def _reader(self):
1448
1449 threading.currentThread().setName('redr')
1450
1451 if __debug__: self._log(1, 'starting using poll')
1452
1453 def poll_error(state):
1454 PollErrors = {
1455 select.POLLERR: 'Error',
1456 select.POLLHUP: 'Hang up',
1457 select.POLLNVAL: 'Invalid request: descriptor not open',
1458 }
1459 return ' '.join([PollErrors[s] for s in PollErrors.keys() if (s & state)])
1460
1461 line_part = ''
1462
1463 poll = select.poll()
1464
1465 poll.register(self.read_fd, select.POLLIN)
1466
1467 while not self.Terminate:
1468 if self.state == LOGOUT:
1469 timeout = 1
1470 else:
1471 timeout = None
1472 try:
1473 r = poll.poll(timeout)
1474 if __debug__: self._log(5, 'poll => %s' % `r`)
1475 if not r:
1476 continue # Timeout
1477
1478 fd,state = r[0]
1479
1480 if state & select.POLLIN:
1481 data = self.read(32768) # Drain ssl buffer if present
1482 start = 0
1483 dlen = len(data)
1484 if __debug__: self._log(5, 'rcvd %s' % dlen)
1485 if dlen == 0:
1486 time.sleep(0.1)
1487 while True:
1488 stop = data.find('\n', start)
1489 if stop < 0:
1490 line_part += data[start:]
1491 break
1492 stop += 1
1493 line_part, start, line = \
1494 '', stop, line_part + data[start:stop]
1495 if __debug__: self._log(4, '< %s' % line)
1496 self.inq.put(line)
1497
1498 if state & ~(select.POLLIN):
1499 raise IOError(poll_error(state))
1500 except:
1501 reason = 'socket error: %s - %s' % sys.exc_info()[:2]
1502 if __debug__:
1503 if not self.Terminate:
1504 self._print_log()
1505 if self.debug: self.debug += 4 # Output all
1506 self._log(1, reason)
1507 self.inq.put((self.abort, reason))
1508 break
1509
1510 poll.unregister(self.read_fd)
1511
1512 if __debug__: self._log(1, 'finished')
1513
1514 else:
1515
1516 # No "poll" - use select()
1517
1518 def _reader(self):
1519
1520 threading.currentThread().setName('redr')
1521
1522 if __debug__: self._log(1, 'starting using select')
1523
1524 line_part = ''
1525
1526 while not self.Terminate:
1527 if self.state == LOGOUT:
1528 timeout = 1
1529 else:
1530 timeout = None
1531 try:
1532 r,w,e = select.select([self.read_fd], [], [], timeout)
1533 if __debug__: self._log(5, 'select => %s, %s, %s' % (r,w,e))
1534 if not r: # Timeout
1535 continue
1536
1537 data = self.read(32768) # Drain ssl buffer if present
1538 start = 0
1539 dlen = len(data)
1540 if __debug__: self._log(5, 'rcvd %s' % dlen)
1541 if dlen == 0:
1542 time.sleep(0.1)
1543 while True:
1544 stop = data.find('\n', start)
1545 if stop < 0:
1546 line_part += data[start:]
1547 break
1548 stop += 1
1549 line_part, start, line = \
1550 '', stop, line_part + data[start:stop]
1551 if __debug__: self._log(4, '< %s' % line)
1552 self.inq.put(line)
1553 except:
1554 reason = 'socket error: %s - %s' % sys.exc_info()[:2]
1555 if __debug__:
1556 if not self.Terminate:
1557 self._print_log()
1558 if self.debug: self.debug += 4 # Output all
1559 self._log(1, reason)
1560 self.inq.put((self.abort, reason))
1561 break
1562
1563 if __debug__: self._log(1, 'finished')
1564
1565
1566 def _writer(self):
1567
1568 threading.currentThread().setName('wrtr')
1569
1570 if __debug__: self._log(1, 'starting')
1571
1572 reason = 'Terminated'
1573
1574 while not self.Terminate:
1575 rqb = self.ouq.get()
1576 if rqb is None:
1577 break # Outq flushed
1578
1579 try:
1580 self.send(rqb.data)
1581 if __debug__: self._log(4, '> %s' % rqb.data)
1582 except:
1583 reason = 'socket error: %s - %s' % sys.exc_info()[:2]
1584 if __debug__:
1585 if not self.Terminate:
1586 self._print_log()
1587 if self.debug: self.debug += 4 # Output all
1588 self._log(1, reason)
1589 rqb.abort(self.abort, reason)
1590 break
1591
1592 self.inq.put((self.abort, reason))
1593
1594 if __debug__: self._log(1, 'finished')
1595
1596
1597
1598 # Debugging
1599
1600
1601 if __debug__:
1602
1603 def _init_debug(self, debug=None, debug_file=None):
1604 self.debug = debug is not None and debug or Debug is not None and Debug or 0
1605 self.debug_file = debug_file is not None and debug_file or sys.stderr
1606
1607 self.debug_lock = threading.Lock()
1608 self._cmd_log_len = 20
1609 self._cmd_log_idx = 0
1610 self._cmd_log = {} # Last `_cmd_log_len' interactions
1611 if self.debug:
1612 self._mesg('imaplib2 version %s' % __version__)
1613 self._mesg('imaplib2 debug level %s' % self.debug)
1614
1615
1616 def _dump_ur(self, lvl):
1617 if lvl > self.debug:
1618 return
1619
1620 l = self.untagged_responses.items()
1621 if not l:
1622 return
1623
1624 t = '\n\t\t'
1625 l = map(lambda x:'%s: "%s"' % (x[0], x[1][0] and '" "'.join(x[1]) or ''), l)
1626 self.debug_lock.acquire()
1627 self._mesg('untagged responses dump:%s%s' % (t, t.join(l)))
1628 self.debug_lock.release()
1629
1630
1631 def _log(self, lvl, line):
1632 if lvl > self.debug:
1633 return
1634
1635 if line[-2:] == CRLF:
1636 line = line[:-2] + '\\r\\n'
1637
1638 tn = threading.currentThread().getName()
1639
1640 if self.debug >= 4:
1641 self.debug_lock.acquire()
1642 self._mesg(line, tn)
1643 self.debug_lock.release()
1644 return
1645
1646 # Keep log of last `_cmd_log_len' interactions for debugging.
1647 self._cmd_log[self._cmd_log_idx] = (line, tn, time.time())
1648 self._cmd_log_idx += 1
1649 if self._cmd_log_idx >= self._cmd_log_len:
1650 self._cmd_log_idx = 0
1651
1652
1653 def _mesg(self, s, tn=None, secs=None):
1654 if secs is None:
1655 secs = time.time()
1656 if tn is None:
1657 tn = threading.currentThread().getName()
1658 tm = time.strftime('%M:%S', time.localtime(secs))
1659 self.debug_file.write(' %s.%02d %s %s\n' % (tm, (secs*100)%100, tn, s))
1660 self.debug_file.flush()
1661
1662
1663 def _print_log(self):
1664 self.debug_lock.acquire()
1665 i, n = self._cmd_log_idx, self._cmd_log_len
1666 if n: self._mesg('last %d imaplib2 reports:' % n)
1667 while n:
1668 try:
1669 self._mesg(*self._cmd_log[i])
1670 except:
1671 pass
1672 i += 1
1673 if i >= self._cmd_log_len:
1674 i = 0
1675 n -= 1
1676 self.debug_lock.release()
1677
1678
1679
1680 class IMAP4_SSL(IMAP4):
1681
1682 """IMAP4 client class over SSL connection
1683
1684 Instantiate with:
1685 IMAP4_SSL(host=None, port=None, keyfile=None, certfile=None, debug=None, debug_file=None)
1686
1687 host - host's name (default: localhost);
1688 port - port number (default: standard IMAP4 SSL port);
1689 keyfile - PEM formatted file that contains your private key (default: None);
1690 certfile - PEM formatted certificate chain file (default: None);
1691 debug - debug level (default: 0 - no debug);
1692 debug_file - debug stream (default: sys.stderr).
1693
1694 For more documentation see the docstring of the parent class IMAP4.
1695 """
1696
1697
1698 def __init__(self, host=None, port=None, keyfile=None, certfile=None, debug=None, debug_file=None):
1699 self.keyfile = keyfile
1700 self.certfile = certfile
1701 IMAP4.__init__(self, host, port, debug, debug_file)
1702
1703
1704 def open(self, host=None, port=None):
1705 """open(host=None, port=None)
1706 Setup secure connection to remote server on "host:port"
1707 (default: localhost:standard IMAP4 SSL port).
1708 This connection will be used by the routines:
1709 read, send, shutdown, socket, ssl."""
1710
1711 self.host = host is not None and host or ''
1712 self.port = port is not None and port or IMAP4_SSL_PORT
1713 self.sock = self.open_socket()
1714
1715 try:
1716 import ssl
1717 self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
1718 except ImportError:
1719 self.sslobj = socket.ssl(self.sock, self.keyfile, self.certfile)
1720
1721 self.read_fd = self.sock.fileno()
1722
1723
1724 def read(self, size):
1725 """data = read(size)
1726 Read at most 'size' bytes from remote."""
1727
1728 if self.decompressor is None:
1729 return self.sslobj.read(size)
1730
1731 if self.decompressor.unconsumed_tail:
1732 data = self.decompressor.unconsumed_tail
1733 else:
1734 data = self.sslobj.read(8192)
1735
1736 return self.decompressor.decompress(data, size)
1737
1738
1739 def send(self, data):
1740 """send(data)
1741 Send 'data' to remote."""
1742
1743 if self.compressor is not None:
1744 data = self.compressor.compress(data)
1745 data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
1746
1747 # NB: socket.ssl needs a "sendall" method to match socket objects.
1748 bytes = len(data)
1749 while bytes > 0:
1750 sent = self.sslobj.write(data)
1751 if sent == bytes:
1752 break # avoid copy
1753 data = data[sent:]
1754 bytes = bytes - sent
1755
1756
1757 def ssl(self):
1758 """ssl = ssl()
1759 Return socket.ssl instance used to communicate with the IMAP4 server."""
1760
1761 return self.sslobj
1762
1763
1764
1765 class IMAP4_stream(IMAP4):
1766
1767 """IMAP4 client class over a stream
1768
1769 Instantiate with:
1770 IMAP4_stream(command, debug=None, debug_file=None)
1771
1772 command - string that can be passed to os.popen2();
1773 debug - debug level (default: 0 - no debug);
1774 debug_file - debug stream (default: sys.stderr).
1775
1776 For more documentation see the docstring of the parent class IMAP4.
1777 """
1778
1779
1780 def __init__(self, command, debug=None, debug_file=None):
1781 self.command = command
1782 self.host = command
1783 self.port = None
1784 self.sock = None
1785 self.writefile, self.readfile = None, None
1786 self.read_fd = None
1787 IMAP4.__init__(self, debug=debug, debug_file=debug_file)
1788
1789
1790 def open(self, host=None, port=None):
1791 """open(host=None, port=None)
1792 Setup a stream connection via 'self.command'.
1793 This connection will be used by the routines:
1794 read, send, shutdown, socket."""
1795
1796 self.writefile, self.readfile = os.popen2(self.command)
1797 self.read_fd = self.readfile.fileno()
1798
1799
1800 def read(self, size):
1801 """Read 'size' bytes from remote."""
1802
1803 if self.decompressor is None:
1804 return os.read(self.read_fd, size)
1805
1806 if self.decompressor.unconsumed_tail:
1807 data = self.decompressor.unconsumed_tail
1808 else:
1809 data = os.read(self.read_fd, 8192)
1810
1811 return self.decompressor.decompress(data, size)
1812
1813
1814 def send(self, data):
1815 """Send data to remote."""
1816
1817 if self.compressor is not None:
1818 data = self.compressor.compress(data)
1819 data += self.compressor.flush(zlib.Z_SYNC_FLUSH)
1820
1821 self.writefile.write(data)
1822 self.writefile.flush()
1823
1824
1825 def shutdown(self):
1826 """Close I/O established in "open"."""
1827
1828 self.readfile.close()
1829 self.writefile.close()
1830
1831
1832
1833 class _Authenticator(object):
1834
1835 """Private class to provide en/de-coding
1836 for base64 authentication conversation."""
1837
1838 def __init__(self, mechinst):
1839 self.mech = mechinst # Callable object to provide/process data
1840
1841 def process(self, data, rqb):
1842 ret = self.mech(self.decode(data))
1843 if ret is None:
1844 return '*' # Abort conversation
1845 return self.encode(ret)
1846
1847 def encode(self, inp):
1848 #
1849 # Invoke binascii.b2a_base64 iteratively with
1850 # short even length buffers, strip the trailing
1851 # line feed from the result and append. "Even"
1852 # means a number that factors to both 6 and 8,
1853 # so when it gets to the end of the 8-bit input
1854 # there's no partial 6-bit output.
1855 #
1856 oup = ''
1857 while inp:
1858 if len(inp) > 48:
1859 t = inp[:48]
1860 inp = inp[48:]
1861 else:
1862 t = inp
1863 inp = ''
1864 e = binascii.b2a_base64(t)
1865 if e:
1866 oup = oup + e[:-1]
1867 return oup
1868
1869 def decode(self, inp):
1870 if not inp:
1871 return ''
1872 return binascii.a2b_base64(inp)
1873
1874
1875
1876
1877 class _IdleCont(object):
1878
1879 """When process is called, server is in IDLE state
1880 and will send asynchronous changes."""
1881
1882 def __init__(self, parent, timeout):
1883 self.parent = parent
1884 self.timeout = timeout is not None and timeout or IDLE_TIMEOUT
1885 self.parent.idle_timeout = self.timeout + time.time()
1886
1887 def process(self, data, rqb):
1888 self.parent.idle_rqb = rqb
1889 self.parent.idle_timeout = self.timeout + time.time()
1890 if __debug__: self.parent._log(2, 'server IDLE started, timeout in %.2f secs' % self.timeout)
1891 return None
1892
1893
1894
1895 Mon2num = {'Jan': 1, 'Feb': 2, 'Mar': 3, 'Apr': 4, 'May': 5, 'Jun': 6,
1896 'Jul': 7, 'Aug': 8, 'Sep': 9, 'Oct': 10, 'Nov': 11, 'Dec': 12}
1897
1898 InternalDate = re.compile(r'.*INTERNALDATE "'
1899 r'(?P<day>[ 0123][0-9])-(?P<mon>[A-Z][a-z][a-z])-(?P<year>[0-9][0-9][0-9][0-9])'
1900 r' (?P<hour>[0-9][0-9]):(?P<min>[0-9][0-9]):(?P<sec>[0-9][0-9])'
1901 r' (?P<zonen>[-+])(?P<zoneh>[0-9][0-9])(?P<zonem>[0-9][0-9])'
1902 r'"')
1903
1904
1905 def Internaldate2Time(resp):
1906
1907 """time_tuple = Internaldate2Time(resp)
1908 Convert IMAP4 INTERNALDATE to UT."""
1909
1910 mo = InternalDate.match(resp)
1911 if not mo:
1912 return None
1913
1914 mon = Mon2num[mo.group('mon')]
1915 zonen = mo.group('zonen')
1916
1917 day = int(mo.group('day'))
1918 year = int(mo.group('year'))
1919 hour = int(mo.group('hour'))
1920 min = int(mo.group('min'))
1921 sec = int(mo.group('sec'))
1922 zoneh = int(mo.group('zoneh'))
1923 zonem = int(mo.group('zonem'))
1924
1925 # INTERNALDATE timezone must be subtracted to get UT
1926
1927 zone = (zoneh*60 + zonem)*60
1928 if zonen == '-':
1929 zone = -zone
1930
1931 tt = (year, mon, day, hour, min, sec, -1, -1, -1)
1932
1933 utc = time.mktime(tt)
1934
1935 # Following is necessary because the time module has no 'mkgmtime'.
1936 # 'mktime' assumes arg in local timezone, so adds timezone/altzone.
1937
1938 lt = time.localtime(utc)
1939 if time.daylight and lt[-1]:
1940 zone = zone + time.altzone
1941 else:
1942 zone = zone + time.timezone
1943
1944 return time.localtime(utc - zone)
1945
1946 Internaldate2tuple = Internaldate2Time # (Backward compatible)
1947
1948
1949
1950 def Time2Internaldate(date_time):
1951
1952 """'"DD-Mmm-YYYY HH:MM:SS +HHMM"' = Time2Internaldate(date_time)
1953 Convert 'date_time' to IMAP4 INTERNALDATE representation."""
1954
1955 if isinstance(date_time, (int, float)):
1956 tt = time.localtime(date_time)
1957 elif isinstance(date_time, (tuple, time.struct_time)):
1958 tt = date_time
1959 elif isinstance(date_time, str) and (date_time[0],date_time[-1]) == ('"','"'):
1960 return date_time # Assume in correct format
1961 else:
1962 raise ValueError("date_time not of a known type")
1963
1964 dt = time.strftime("%d-%b-%Y %H:%M:%S", tt)
1965 if dt[0] == '0':
1966 dt = ' ' + dt[1:]
1967 if time.daylight and tt[-1]:
1968 zone = -time.altzone
1969 else:
1970 zone = -time.timezone
1971 return '"' + dt + " %+03d%02d" % divmod(zone//60, 60) + '"'
1972
1973
1974
1975 FLAGS_cre = re.compile(r'.*FLAGS \((?P<flags>[^\)]*)\)')
1976
1977 def ParseFlags(resp):
1978
1979 """('flag', ...) = ParseFlags(line)
1980 Convert IMAP4 flags response to python tuple."""
1981
1982 mo = FLAGS_cre.match(resp)
1983 if not mo:
1984 return ()
1985
1986 return tuple(mo.group('flags').split())
1987
1988
1989
1990 if __name__ == '__main__':
1991
1992 # To test: invoke either as 'python imaplib2.py [IMAP4_server_hostname]',
1993 # or as 'python imaplib2.py -s "rsh IMAP4_server_hostname exec /etc/rimapd"'
1994 # or as 'python imaplib2.py -l "keyfile[:certfile]" [IMAP4_SSL_server_hostname]'
1995
1996 import getopt, getpass
1997
1998 try:
1999 optlist, args = getopt.getopt(sys.argv[1:], 'd:l:s:p:v')
2000 except getopt.error, val:
2001 optlist, args = (), ()
2002
2003 debug, port, stream_command, keyfile, certfile = (None,)*5
2004 for opt,val in optlist:
2005 if opt == '-d':
2006 debug = int(val)
2007 elif opt == '-l':
2008 try:
2009 keyfile,certfile = val.split(':')
2010 except ValueError:
2011 keyfile,certfile = val,val
2012 elif opt == '-p':
2013 port = int(val)
2014 elif opt == '-s':
2015 stream_command = val
2016 if not args: args = (stream_command,)
2017 elif opt == '-v':
2018 print __version__
2019 sys.exit(0)
2020
2021 if not args: args = ('',)
2022 if not port: port = (keyfile is not None) and IMAP4_SSL_PORT or IMAP4_PORT
2023
2024 host = args[0]
2025
2026 USER = getpass.getuser()
2027
2028 test_mesg = 'From: %(user)s@localhost%(lf)sSubject: IMAP4 test%(lf)s%(lf)s%(data)s' \
2029 % {'user':USER, 'lf':'\n', 'data':open(__file__).read()}
2030 test_seq1 = [
2031 ('list', ('""', '%')),
2032 ('create', ('/tmp/imaplib2_test.0',)),
2033 ('rename', ('/tmp/imaplib2_test.0', '/tmp/imaplib2_test.1')),
2034 ('CREATE', ('/tmp/imaplib2_test.2',)),
2035 ('append', ('/tmp/imaplib2_test.2', None, None, test_mesg)),
2036 ('list', ('/tmp', 'imaplib2_test*')),
2037 ('select', ('/tmp/imaplib2_test.2',)),
2038 ('search', (None, 'SUBJECT', 'IMAP4 test')),
2039 ('fetch', ('1', '(FLAGS INTERNALDATE RFC822)')),
2040 ('store', ('1', 'FLAGS', '(\Deleted)')),
2041 ('namespace', ()),
2042 ('expunge', ()),
2043 ('recent', ()),
2044 ('close', ()),
2045 ]
2046
2047 test_seq2 = (
2048 ('select', ()),
2049 ('response',('UIDVALIDITY',)),
2050 ('response', ('EXISTS',)),
2051 ('append', (None, None, None, test_mesg)),
2052 ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')),
2053 ('uid', ('SEARCH', 'ALL')),
2054 ('uid', ('THREAD', 'references', 'UTF-8', '(SEEN)')),
2055 ('recent', ()),
2056 )
2057
2058 AsyncError = None
2059
2060 def responder((response, cb_arg, error)):
2061 global AsyncError
2062 cmd, args = cb_arg
2063 if error is not None:
2064 AsyncError = error
2065 M._mesg('[cb] ERROR %s %.100s => %s' % (cmd, args, error))
2066 return
2067 typ, dat = response
2068 M._mesg('[cb] %s %.100s => %s %.100s' % (cmd, args, typ, dat))
2069 if typ == 'NO':
2070 AsyncError = (Exception, dat[0])
2071
2072 def run(cmd, args, cb=None):
2073 if AsyncError:
2074 M.logout()
2075 typ, val = AsyncError
2076 raise typ(val)
2077 M._mesg('%s %.100s' % (cmd, args))
2078 try:
2079 if cb is not None:
2080 typ, dat = getattr(M, cmd)(callback=responder, cb_arg=(cmd, args), *args)
2081 if M.debug:
2082 M._mesg('%s %.100s => %s %.100s' % (cmd, args, typ, dat))
2083 else:
2084 typ, dat = getattr(M, cmd)(*args)
2085 M._mesg('%s %.100s => %s %.100s' % (cmd, args, typ, dat))
2086 except:
2087 M.logout()
2088 raise
2089 if typ == 'NO':
2090 M.logout()
2091 raise Exception(dat[0])
2092 return dat
2093
2094 try:
2095 threading.currentThread().setName('main')
2096
2097 if keyfile is not None:
2098 if not keyfile: keyfile = None
2099 if not certfile: certfile = None
2100 M = IMAP4_SSL(host=host, port=port, keyfile=keyfile, certfile=certfile, debug=debug)
2101 elif stream_command:
2102 M = IMAP4_stream(stream_command, debug=debug)
2103 else:
2104 M = IMAP4(host=host, port=port, debug=debug)
2105 if M.state != 'AUTH': # Login needed
2106 PASSWD = getpass.getpass("IMAP password for %s on %s: " % (USER, host or "localhost"))
2107 test_seq1.insert(0, ('login', (USER, PASSWD)))
2108 M._mesg('PROTOCOL_VERSION = %s' % M.PROTOCOL_VERSION)
2109 M._mesg('CAPABILITIES = %r' % (M.capabilities,))
2110 if 'COMPRESS=DEFLATE' in M.capabilities:
2111 M.enable_compression()
2112
2113 for cmd,args in test_seq1:
2114 run(cmd, args, cb=1)
2115
2116 for ml in run('list', ('/tmp/', 'imaplib2_test%')):
2117 mo = re.match(r'.*"([^"]+)"$', ml)
2118 if mo: path = mo.group(1)
2119 else: path = ml.split()[-1]
2120 run('delete', (path,), cb=1)
2121
2122 for cmd,args in test_seq2:
2123 if (cmd,args) != ('uid', ('SEARCH', 'SUBJECT', 'IMAP4 test')):
2124 run(cmd, args, cb=1)
2125 continue
2126
2127 dat = run(cmd, args)
2128 uid = dat[-1].split()
2129 if not uid: continue
2130 run('uid', ('FETCH', uid[-1],
2131 '(FLAGS INTERNALDATE RFC822.SIZE RFC822.HEADER RFC822.TEXT)'), cb=1)
2132 run('uid', ('STORE', uid[-1], 'FLAGS', '(\Deleted)'), cb=1)
2133 run('expunge', (), cb=1)
2134
2135 run('idle', (3,))
2136 run('logout', ())
2137
2138 if debug:
2139 print
2140 M._print_log()
2141
2142 print '\nAll tests OK.'
2143
2144 except:
2145 print '\nTests failed.'
2146
2147 if not debug:
2148 print '''
2149 If you would like to see debugging output,
2150 try: %s -d5
2151 ''' % sys.argv[0]
2152
2153 raise