OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.mail.test.test_imap -*- | |
2 # Copyright (c) 2001-2008 Twisted Matrix Laboratories. | |
3 # See LICENSE for details. | |
4 | |
5 | |
6 """ | |
7 An IMAP4 protocol implementation | |
8 | |
9 @author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>} | |
10 | |
11 To do:: | |
12 Suspend idle timeout while server is processing | |
13 Use an async message parser instead of buffering in memory | |
14 Figure out a way to not queue multi-message client requests (Flow? A simple ca
llback?) | |
15 Clarify some API docs (Query, etc) | |
16 Make APPEND recognize (again) non-existent mailboxes before accepting the lite
ral | |
17 """ | |
18 | |
19 import rfc822 | |
20 import base64 | |
21 import binascii | |
22 import hmac | |
23 import re | |
24 import tempfile | |
25 import string | |
26 import time | |
27 import random | |
28 import types | |
29 | |
30 import email.Utils | |
31 | |
32 try: | |
33 import cStringIO as StringIO | |
34 except: | |
35 import StringIO | |
36 | |
37 from zope.interface import implements, Interface | |
38 | |
39 from twisted.protocols import basic | |
40 from twisted.protocols import policies | |
41 from twisted.internet import defer | |
42 from twisted.internet import error | |
43 from twisted.internet.defer import maybeDeferred | |
44 from twisted.python import log, text | |
45 from twisted.internet import interfaces | |
46 | |
47 from twisted import cred | |
48 import twisted.cred.error | |
49 import twisted.cred.credentials | |
50 | |
51 class MessageSet(object): | |
52 """ | |
53 Essentially an infinite bitfield, with some extra features. | |
54 | |
55 @type getnext: Function taking C{int} returning C{int} | |
56 @ivar getnext: A function that returns the next message number, | |
57 used when iterating through the MessageSet. By default, a function | |
58 returning the next integer is supplied, but as this can be rather | |
59 inefficient for sparse UID iterations, it is recommended to supply | |
60 one when messages are requested by UID. The argument is provided | |
61 as a hint to the implementation and may be ignored if it makes sense | |
62 to do so (eg, if an iterator is being used that maintains its own | |
63 state, it is guaranteed that it will not be called out-of-order). | |
64 """ | |
65 _empty = [] | |
66 | |
67 def __init__(self, start=_empty, end=_empty): | |
68 """ | |
69 Create a new MessageSet() | |
70 | |
71 @type start: Optional C{int} | |
72 @param start: Start of range, or only message number | |
73 | |
74 @type end: Optional C{int} | |
75 @param end: End of range. | |
76 """ | |
77 self._last = self._empty # Last message/UID in use | |
78 self.ranges = [] # List of ranges included | |
79 self.getnext = lambda x: x+1 # A function which will return the next | |
80 # message id. Handy for UID requests. | |
81 | |
82 if start is self._empty: | |
83 return | |
84 | |
85 if isinstance(start, types.ListType): | |
86 self.ranges = start[:] | |
87 self.clean() | |
88 else: | |
89 self.add(start,end) | |
90 | |
91 # Ooo. A property. | |
92 def last(): | |
93 def _setLast(self,value): | |
94 if self._last is not self._empty: | |
95 raise ValueError("last already set") | |
96 | |
97 self._last = value | |
98 for i,(l,h) in enumerate(self.ranges): | |
99 if l is not None: | |
100 break # There are no more Nones after this | |
101 l = value | |
102 if h is None: | |
103 h = value | |
104 if l > h: | |
105 l, h = h, l | |
106 self.ranges[i] = (l,h) | |
107 | |
108 self.clean() | |
109 | |
110 def _getLast(self): | |
111 return self._last | |
112 | |
113 doc = ''' | |
114 "Highest" message number, refered to by "*". | |
115 Must be set before attempting to use the MessageSet. | |
116 ''' | |
117 return _getLast, _setLast, None, doc | |
118 last = property(*last()) | |
119 | |
120 def add(self, start, end=_empty): | |
121 """ | |
122 Add another range | |
123 | |
124 @type start: C{int} | |
125 @param start: Start of range, or only message number | |
126 | |
127 @type end: Optional C{int} | |
128 @param end: End of range. | |
129 """ | |
130 if end is self._empty: | |
131 end = start | |
132 | |
133 if self._last is not self._empty: | |
134 if start is None: | |
135 start = self.last | |
136 if end is None: | |
137 end = self.last | |
138 | |
139 if start > end: | |
140 # Try to keep in low, high order if possible | |
141 # (But we don't know what None means, this will keep | |
142 # None at the start of the ranges list) | |
143 start, end = end, start | |
144 | |
145 self.ranges.append((start,end)) | |
146 self.clean() | |
147 | |
148 def __add__(self, other): | |
149 if isinstance(other, MessageSet): | |
150 ranges = self.ranges + other.ranges | |
151 return MessageSet(ranges) | |
152 else: | |
153 res = MessageSet(self.ranges) | |
154 try: | |
155 res.add(*other) | |
156 except TypeError: | |
157 res.add(other) | |
158 return res | |
159 | |
160 def extend(self, other): | |
161 if isinstance(other, MessageSet): | |
162 self.ranges.extend(other.ranges) | |
163 self.clean() | |
164 else: | |
165 try: | |
166 self.add(*other) | |
167 except TypeError: | |
168 self.add(other) | |
169 | |
170 return self | |
171 | |
172 def clean(self): | |
173 """ | |
174 Clean ranges list, combining adjacent ranges | |
175 """ | |
176 | |
177 self.ranges.sort() | |
178 | |
179 oldl, oldh = None, None | |
180 for i,(l,h) in enumerate(self.ranges): | |
181 if l is None: | |
182 continue | |
183 # l is >= oldl and h is >= oldh due to sort() | |
184 if oldl is not None and l <= oldh+1: | |
185 l = oldl | |
186 h = max(oldh,h) | |
187 self.ranges[i-1] = None | |
188 self.ranges[i] = (l,h) | |
189 | |
190 oldl,oldh = l,h | |
191 | |
192 self.ranges = filter(None, self.ranges) | |
193 | |
194 def __contains__(self, value): | |
195 """ | |
196 May raise TypeError if we encounter unknown "high" values | |
197 """ | |
198 for l,h in self.ranges: | |
199 if l is None: | |
200 raise TypeError( | |
201 "Can't determine membership; last value not set") | |
202 if l <= value <= h: | |
203 return True | |
204 | |
205 return False | |
206 | |
207 def _iterator(self): | |
208 for l,h in self.ranges: | |
209 l = self.getnext(l-1) | |
210 while l <= h: | |
211 yield l | |
212 l = self.getnext(l) | |
213 if l is None: | |
214 break | |
215 | |
216 def __iter__(self): | |
217 if self.ranges and self.ranges[0][0] is None: | |
218 raise TypeError("Can't iterate; last value not set") | |
219 | |
220 return self._iterator() | |
221 | |
222 def __len__(self): | |
223 res = 0 | |
224 for l, h in self.ranges: | |
225 if l is None: | |
226 raise TypeError("Can't size object; last value not set") | |
227 res += (h - l) + 1 | |
228 | |
229 return res | |
230 | |
231 def __str__(self): | |
232 p = [] | |
233 for low, high in self.ranges: | |
234 if low == high: | |
235 if low is None: | |
236 p.append('*') | |
237 else: | |
238 p.append(str(low)) | |
239 elif low is None: | |
240 p.append('%d:*' % (high,)) | |
241 else: | |
242 p.append('%d:%d' % (low, high)) | |
243 return ','.join(p) | |
244 | |
245 def __repr__(self): | |
246 return '<MessageSet %s>' % (str(self),) | |
247 | |
248 def __eq__(self, other): | |
249 if isinstance(other, MessageSet): | |
250 return self.ranges == other.ranges | |
251 return False | |
252 | |
253 | |
254 class LiteralString: | |
255 def __init__(self, size, defered): | |
256 self.size = size | |
257 self.data = [] | |
258 self.defer = defered | |
259 | |
260 def write(self, data): | |
261 self.size -= len(data) | |
262 passon = None | |
263 if self.size > 0: | |
264 self.data.append(data) | |
265 else: | |
266 if self.size: | |
267 data, passon = data[:self.size], data[self.size:] | |
268 else: | |
269 passon = '' | |
270 if data: | |
271 self.data.append(data) | |
272 return passon | |
273 | |
274 def callback(self, line): | |
275 """ | |
276 Call defered with data and rest of line | |
277 """ | |
278 self.defer.callback((''.join(self.data), line)) | |
279 | |
280 class LiteralFile: | |
281 _memoryFileLimit = 1024 * 1024 * 10 | |
282 | |
283 def __init__(self, size, defered): | |
284 self.size = size | |
285 self.defer = defered | |
286 if size > self._memoryFileLimit: | |
287 self.data = tempfile.TemporaryFile() | |
288 else: | |
289 self.data = StringIO.StringIO() | |
290 | |
291 def write(self, data): | |
292 self.size -= len(data) | |
293 passon = None | |
294 if self.size > 0: | |
295 self.data.write(data) | |
296 else: | |
297 if self.size: | |
298 data, passon = data[:self.size], data[self.size:] | |
299 else: | |
300 passon = '' | |
301 if data: | |
302 self.data.write(data) | |
303 return passon | |
304 | |
305 def callback(self, line): | |
306 """ | |
307 Call defered with data and rest of line | |
308 """ | |
309 self.data.seek(0,0) | |
310 self.defer.callback((self.data, line)) | |
311 | |
312 | |
313 class WriteBuffer: | |
314 """Buffer up a bunch of writes before sending them all to a transport at onc
e. | |
315 """ | |
316 def __init__(self, transport, size=8192): | |
317 self.bufferSize = size | |
318 self.transport = transport | |
319 self._length = 0 | |
320 self._writes = [] | |
321 | |
322 def write(self, s): | |
323 self._length += len(s) | |
324 self._writes.append(s) | |
325 if self._length > self.bufferSize: | |
326 self.flush() | |
327 | |
328 def flush(self): | |
329 if self._writes: | |
330 self.transport.writeSequence(self._writes) | |
331 self._writes = [] | |
332 self._length = 0 | |
333 | |
334 | |
335 class Command: | |
336 _1_RESPONSES = ('CAPABILITY', 'FLAGS', 'LIST', 'LSUB', 'STATUS', 'SEARCH', '
NAMESPACE') | |
337 _2_RESPONSES = ('EXISTS', 'EXPUNGE', 'FETCH', 'RECENT') | |
338 _OK_RESPONSES = ('UIDVALIDITY', 'READ-WRITE', 'READ-ONLY', 'UIDNEXT', 'PERMA
NENTFLAGS') | |
339 defer = None | |
340 | |
341 def __init__(self, command, args=None, wantResponse=(), | |
342 continuation=None, *contArgs, **contKw): | |
343 self.command = command | |
344 self.args = args | |
345 self.wantResponse = wantResponse | |
346 self.continuation = lambda x: continuation(x, *contArgs, **contKw) | |
347 self.lines = [] | |
348 | |
349 def format(self, tag): | |
350 if self.args is None: | |
351 return ' '.join((tag, self.command)) | |
352 return ' '.join((tag, self.command, self.args)) | |
353 | |
354 def finish(self, lastLine, unusedCallback): | |
355 send = [] | |
356 unuse = [] | |
357 for L in self.lines: | |
358 names = parseNestedParens(L) | |
359 N = len(names) | |
360 if (N >= 1 and names[0] in self._1_RESPONSES or | |
361 N >= 2 and names[0] == 'OK' and isinstance(names[1], types.ListT
ype) and names[1][0] in self._OK_RESPONSES): | |
362 send.append(L) | |
363 elif N >= 3 and names[1] in self._2_RESPONSES: | |
364 if isinstance(names[2], list) and len(names[2]) >= 1 and names[2
][0] == 'FLAGS' and 'FLAGS' not in self.args: | |
365 unuse.append(L) | |
366 else: | |
367 send.append(L) | |
368 elif N >= 2 and names[1] in self._2_RESPONSES: | |
369 send.append(L) | |
370 else: | |
371 unuse.append(L) | |
372 d, self.defer = self.defer, None | |
373 d.callback((send, lastLine)) | |
374 if unuse: | |
375 unusedCallback(unuse) | |
376 | |
377 class LOGINCredentials(cred.credentials.UsernamePassword): | |
378 def __init__(self): | |
379 self.challenges = ['Password\0', 'User Name\0'] | |
380 self.responses = ['password', 'username'] | |
381 cred.credentials.UsernamePassword.__init__(self, None, None) | |
382 | |
383 def getChallenge(self): | |
384 return self.challenges.pop() | |
385 | |
386 def setResponse(self, response): | |
387 setattr(self, self.responses.pop(), response) | |
388 | |
389 def moreChallenges(self): | |
390 return bool(self.challenges) | |
391 | |
392 class PLAINCredentials(cred.credentials.UsernamePassword): | |
393 def __init__(self): | |
394 cred.credentials.UsernamePassword.__init__(self, None, None) | |
395 | |
396 def getChallenge(self): | |
397 return '' | |
398 | |
399 def setResponse(self, response): | |
400 parts = response[:-1].split('\0', 1) | |
401 if len(parts) != 2: | |
402 raise IllegalClientResponse("Malformed Response - wrong number of pa
rts") | |
403 self.username, self.password = parts | |
404 | |
405 def moreChallenges(self): | |
406 return False | |
407 | |
408 class IMAP4Exception(Exception): | |
409 def __init__(self, *args): | |
410 Exception.__init__(self, *args) | |
411 | |
412 class IllegalClientResponse(IMAP4Exception): pass | |
413 | |
414 class IllegalOperation(IMAP4Exception): pass | |
415 | |
416 class IllegalMailboxEncoding(IMAP4Exception): pass | |
417 | |
418 class IMailboxListener(Interface): | |
419 """Interface for objects interested in mailbox events""" | |
420 | |
421 def modeChanged(writeable): | |
422 """Indicates that the write status of a mailbox has changed. | |
423 | |
424 @type writeable: C{bool} | |
425 @param writeable: A true value if write is now allowed, false | |
426 otherwise. | |
427 """ | |
428 | |
429 def flagsChanged(newFlags): | |
430 """Indicates that the flags of one or more messages have changed. | |
431 | |
432 @type newFlags: C{dict} | |
433 @param newFlags: A mapping of message identifiers to tuples of flags | |
434 now set on that message. | |
435 """ | |
436 | |
437 def newMessages(exists, recent): | |
438 """Indicates that the number of messages in a mailbox has changed. | |
439 | |
440 @type exists: C{int} or C{None} | |
441 @param exists: The total number of messages now in this mailbox. | |
442 If the total number of messages has not changed, this should be | |
443 C{None}. | |
444 | |
445 @type recent: C{int} | |
446 @param recent: The number of messages now flagged \\Recent. | |
447 If the number of recent messages has not changed, this should be | |
448 C{None}. | |
449 """ | |
450 | |
451 class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin): | |
452 """ | |
453 Protocol implementation for an IMAP4rev1 server. | |
454 | |
455 The server can be in any of four states: | |
456 - Non-authenticated | |
457 - Authenticated | |
458 - Selected | |
459 - Logout | |
460 """ | |
461 implements(IMailboxListener) | |
462 | |
463 # Identifier for this server software | |
464 IDENT = 'Twisted IMAP4rev1 Ready' | |
465 | |
466 # Number of seconds before idle timeout | |
467 # Initially 1 minute. Raised to 30 minutes after login. | |
468 timeOut = 60 | |
469 | |
470 POSTAUTH_TIMEOUT = 60 * 30 | |
471 | |
472 # Whether STARTTLS has been issued successfully yet or not. | |
473 startedTLS = False | |
474 | |
475 # Whether our transport supports TLS | |
476 canStartTLS = False | |
477 | |
478 # Mapping of tags to commands we have received | |
479 tags = None | |
480 | |
481 # The object which will handle logins for us | |
482 portal = None | |
483 | |
484 # The account object for this connection | |
485 account = None | |
486 | |
487 # Logout callback | |
488 _onLogout = None | |
489 | |
490 # The currently selected mailbox | |
491 mbox = None | |
492 | |
493 # Command data to be processed when literal data is received | |
494 _pendingLiteral = None | |
495 | |
496 # Maximum length to accept for a "short" string literal | |
497 _literalStringLimit = 4096 | |
498 | |
499 # IChallengeResponse factories for AUTHENTICATE command | |
500 challengers = None | |
501 | |
502 state = 'unauth' | |
503 | |
504 parseState = 'command' | |
505 | |
506 def __init__(self, chal = None, contextFactory = None, scheduler = None): | |
507 if chal is None: | |
508 chal = {} | |
509 self.challengers = chal | |
510 self.ctx = contextFactory | |
511 if scheduler is None: | |
512 scheduler = iterateInReactor | |
513 self._scheduler = scheduler | |
514 self._queuedAsync = [] | |
515 | |
516 def capabilities(self): | |
517 cap = {'AUTH': self.challengers.keys()} | |
518 if self.ctx and self.canStartTLS: | |
519 if not self.startedTLS and interfaces.ISSLTransport(self.transport,
None) is None: | |
520 cap['LOGINDISABLED'] = None | |
521 cap['STARTTLS'] = None | |
522 cap['NAMESPACE'] = None | |
523 cap['IDLE'] = None | |
524 return cap | |
525 | |
526 def connectionMade(self): | |
527 self.tags = {} | |
528 self.canStartTLS = interfaces.ITLSTransport(self.transport, None) is not
None | |
529 self.setTimeout(self.timeOut) | |
530 self.sendServerGreeting() | |
531 | |
532 def connectionLost(self, reason): | |
533 self.setTimeout(None) | |
534 if self._onLogout: | |
535 self._onLogout() | |
536 self._onLogout = None | |
537 | |
538 def timeoutConnection(self): | |
539 self.sendLine('* BYE Autologout; connection idle too long') | |
540 self.transport.loseConnection() | |
541 if self.mbox: | |
542 self.mbox.removeListener(self) | |
543 cmbx = ICloseableMailbox(self.mbox, None) | |
544 if cmbx is not None: | |
545 maybeDeferred(cmbx.close).addErrback(log.err) | |
546 self.mbox = None | |
547 self.state = 'timeout' | |
548 | |
549 def rawDataReceived(self, data): | |
550 self.resetTimeout() | |
551 passon = self._pendingLiteral.write(data) | |
552 if passon is not None: | |
553 self.setLineMode(passon) | |
554 | |
555 # Avoid processing commands while buffers are being dumped to | |
556 # our transport | |
557 blocked = None | |
558 | |
559 def _unblock(self): | |
560 commands = self.blocked | |
561 self.blocked = None | |
562 while commands and self.blocked is None: | |
563 self.lineReceived(commands.pop(0)) | |
564 if self.blocked is not None: | |
565 self.blocked.extend(commands) | |
566 | |
567 # def sendLine(self, line): | |
568 # print 'C:', repr(line) | |
569 # return basic.LineReceiver.sendLine(self, line) | |
570 | |
571 def lineReceived(self, line): | |
572 # print 'S:', repr(line) | |
573 if self.blocked is not None: | |
574 self.blocked.append(line) | |
575 return | |
576 | |
577 self.resetTimeout() | |
578 | |
579 f = getattr(self, 'parse_' + self.parseState) | |
580 try: | |
581 f(line) | |
582 except Exception, e: | |
583 self.sendUntaggedResponse('BAD Server error: ' + str(e)) | |
584 log.err() | |
585 | |
586 def parse_command(self, line): | |
587 args = line.split(None, 2) | |
588 rest = None | |
589 if len(args) == 3: | |
590 tag, cmd, rest = args | |
591 elif len(args) == 2: | |
592 tag, cmd = args | |
593 elif len(args) == 1: | |
594 tag = args[0] | |
595 self.sendBadResponse(tag, 'Missing command') | |
596 return None | |
597 else: | |
598 self.sendBadResponse(None, 'Null command') | |
599 return None | |
600 | |
601 cmd = cmd.upper() | |
602 try: | |
603 return self.dispatchCommand(tag, cmd, rest) | |
604 except IllegalClientResponse, e: | |
605 self.sendBadResponse(tag, 'Illegal syntax: ' + str(e)) | |
606 except IllegalOperation, e: | |
607 self.sendNegativeResponse(tag, 'Illegal operation: ' + str(e)) | |
608 except IllegalMailboxEncoding, e: | |
609 self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + str(e)) | |
610 | |
611 def parse_pending(self, line): | |
612 d = self._pendingLiteral | |
613 self._pendingLiteral = None | |
614 self.parseState = 'command' | |
615 d.callback(line) | |
616 | |
617 def dispatchCommand(self, tag, cmd, rest, uid=None): | |
618 f = self.lookupCommand(cmd) | |
619 if f: | |
620 fn = f[0] | |
621 parseargs = f[1:] | |
622 self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid) | |
623 else: | |
624 self.sendBadResponse(tag, 'Unsupported command') | |
625 | |
626 def lookupCommand(self, cmd): | |
627 return getattr(self, '_'.join((self.state, cmd.upper())), None) | |
628 | |
629 def __doCommand(self, tag, handler, args, parseargs, line, uid): | |
630 for (i, arg) in enumerate(parseargs): | |
631 if callable(arg): | |
632 parseargs = parseargs[i+1:] | |
633 maybeDeferred(arg, self, line).addCallback( | |
634 self.__cbDispatch, tag, handler, args, | |
635 parseargs, uid).addErrback(self.__ebDispatch, tag) | |
636 return | |
637 else: | |
638 args.append(arg) | |
639 | |
640 if line: | |
641 # Too many arguments | |
642 raise IllegalClientResponse("Too many arguments for command: " + rep
r(line)) | |
643 | |
644 if uid is not None: | |
645 handler(uid=uid, *args) | |
646 else: | |
647 handler(*args) | |
648 | |
649 def __cbDispatch(self, (arg, rest), tag, fn, args, parseargs, uid): | |
650 args.append(arg) | |
651 self.__doCommand(tag, fn, args, parseargs, rest, uid) | |
652 | |
653 def __ebDispatch(self, failure, tag): | |
654 if failure.check(IllegalClientResponse): | |
655 self.sendBadResponse(tag, 'Illegal syntax: ' + str(failure.value)) | |
656 elif failure.check(IllegalOperation): | |
657 self.sendNegativeResponse(tag, 'Illegal operation: ' + | |
658 str(failure.value)) | |
659 elif failure.check(IllegalMailboxEncoding): | |
660 self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + | |
661 str(failure.value)) | |
662 else: | |
663 self.sendBadResponse(tag, 'Server error: ' + str(failure.value)) | |
664 log.err(failure) | |
665 | |
666 def _stringLiteral(self, size): | |
667 if size > self._literalStringLimit: | |
668 raise IllegalClientResponse( | |
669 "Literal too long! I accept at most %d octets" % | |
670 (self._literalStringLimit,)) | |
671 d = defer.Deferred() | |
672 self.parseState = 'pending' | |
673 self._pendingLiteral = LiteralString(size, d) | |
674 self.sendContinuationRequest('Ready for %d octets of text' % size) | |
675 self.setRawMode() | |
676 return d | |
677 | |
678 def _fileLiteral(self, size): | |
679 d = defer.Deferred() | |
680 self.parseState = 'pending' | |
681 self._pendingLiteral = LiteralFile(size, d) | |
682 self.sendContinuationRequest('Ready for %d octets of data' % size) | |
683 self.setRawMode() | |
684 return d | |
685 | |
686 def arg_astring(self, line): | |
687 """ | |
688 Parse an astring from the line, return (arg, rest), possibly | |
689 via a deferred (to handle literals) | |
690 """ | |
691 line = line.strip() | |
692 if not line: | |
693 raise IllegalClientResponse("Missing argument") | |
694 d = None | |
695 arg, rest = None, None | |
696 if line[0] == '"': | |
697 try: | |
698 spam, arg, rest = line.split('"',2) | |
699 rest = rest[1:] # Strip space | |
700 except ValueError: | |
701 raise IllegalClientResponse("Unmatched quotes") | |
702 elif line[0] == '{': | |
703 # literal | |
704 if line[-1] != '}': | |
705 raise IllegalClientResponse("Malformed literal") | |
706 try: | |
707 size = int(line[1:-1]) | |
708 except ValueError: | |
709 raise IllegalClientResponse("Bad literal size: " + line[1:-1]) | |
710 d = self._stringLiteral(size) | |
711 else: | |
712 arg = line.split(' ',1) | |
713 if len(arg) == 1: | |
714 arg.append('') | |
715 arg, rest = arg | |
716 return d or (arg, rest) | |
717 | |
718 # ATOM: Any CHAR except ( ) { % * " \ ] CTL SP (CHAR is 7bit) | |
719 atomre = re.compile(r'(?P<atom>[^\](){%*"\\\x00-\x20\x80-\xff]+)( (?P<rest>.
*$)|$)') | |
720 | |
721 def arg_atom(self, line): | |
722 """ | |
723 Parse an atom from the line | |
724 """ | |
725 if not line: | |
726 raise IllegalClientResponse("Missing argument") | |
727 m = self.atomre.match(line) | |
728 if m: | |
729 return m.group('atom'), m.group('rest') | |
730 else: | |
731 raise IllegalClientResponse("Malformed ATOM") | |
732 | |
733 def arg_plist(self, line): | |
734 """ | |
735 Parse a (non-nested) parenthesised list from the line | |
736 """ | |
737 if not line: | |
738 raise IllegalClientResponse("Missing argument") | |
739 | |
740 if line[0] != "(": | |
741 raise IllegalClientResponse("Missing parenthesis") | |
742 | |
743 i = line.find(")") | |
744 | |
745 if i == -1: | |
746 raise IllegalClientResponse("Mismatched parenthesis") | |
747 | |
748 return (parseNestedParens(line[1:i],0), line[i+2:]) | |
749 | |
750 def arg_literal(self, line): | |
751 """ | |
752 Parse a literal from the line | |
753 """ | |
754 if not line: | |
755 raise IllegalClientResponse("Missing argument") | |
756 | |
757 if line[0] != '{': | |
758 raise IllegalClientResponse("Missing literal") | |
759 | |
760 if line[-1] != '}': | |
761 raise IllegalClientResponse("Malformed literal") | |
762 | |
763 try: | |
764 size = int(line[1:-1]) | |
765 except ValueError: | |
766 raise IllegalClientResponse("Bad literal size: " + line[1:-1]) | |
767 | |
768 return self._fileLiteral(size) | |
769 | |
770 def arg_searchkeys(self, line): | |
771 """ | |
772 searchkeys | |
773 """ | |
774 query = parseNestedParens(line) | |
775 # XXX Should really use list of search terms and parse into | |
776 # a proper tree | |
777 | |
778 return (query, '') | |
779 | |
780 def arg_seqset(self, line): | |
781 """ | |
782 sequence-set | |
783 """ | |
784 rest = '' | |
785 arg = line.split(' ',1) | |
786 if len(arg) == 2: | |
787 rest = arg[1] | |
788 arg = arg[0] | |
789 | |
790 try: | |
791 return (parseIdList(arg), rest) | |
792 except IllegalIdentifierError, e: | |
793 raise IllegalClientResponse("Bad message number " + str(e)) | |
794 | |
795 def arg_fetchatt(self, line): | |
796 """ | |
797 fetch-att | |
798 """ | |
799 p = _FetchParser() | |
800 p.parseString(line) | |
801 return (p.result, '') | |
802 | |
803 def arg_flaglist(self, line): | |
804 """ | |
805 Flag part of store-att-flag | |
806 """ | |
807 flags = [] | |
808 if line[0] == '(': | |
809 if line[-1] != ')': | |
810 raise IllegalClientResponse("Mismatched parenthesis") | |
811 line = line[1:-1] | |
812 | |
813 while line: | |
814 m = self.atomre.search(line) | |
815 if not m: | |
816 raise IllegalClientResponse("Malformed flag") | |
817 if line[0] == '\\' and m.start() == 1: | |
818 flags.append('\\' + m.group('atom')) | |
819 elif m.start() == 0: | |
820 flags.append(m.group('atom')) | |
821 else: | |
822 raise IllegalClientResponse("Malformed flag") | |
823 line = m.group('rest') | |
824 | |
825 return (flags, '') | |
826 | |
827 def arg_line(self, line): | |
828 """ | |
829 Command line of UID command | |
830 """ | |
831 return (line, '') | |
832 | |
833 def opt_plist(self, line): | |
834 """ | |
835 Optional parenthesised list | |
836 """ | |
837 if line.startswith('('): | |
838 return self.arg_plist(line) | |
839 else: | |
840 return (None, line) | |
841 | |
842 def opt_datetime(self, line): | |
843 """ | |
844 Optional date-time string | |
845 """ | |
846 if line.startswith('"'): | |
847 try: | |
848 spam, date, rest = line.split('"',2) | |
849 except IndexError: | |
850 raise IllegalClientResponse("Malformed date-time") | |
851 return (date, rest[1:]) | |
852 else: | |
853 return (None, line) | |
854 | |
855 def opt_charset(self, line): | |
856 """ | |
857 Optional charset of SEARCH command | |
858 """ | |
859 if line[:7].upper() == 'CHARSET': | |
860 arg = line.split(' ',2) | |
861 if len(arg) == 1: | |
862 raise IllegalClientResponse("Missing charset identifier") | |
863 if len(arg) == 2: | |
864 arg.append('') | |
865 spam, arg, rest = arg | |
866 return (arg, rest) | |
867 else: | |
868 return (None, line) | |
869 | |
870 def sendServerGreeting(self): | |
871 msg = '[CAPABILITY %s] %s' % (' '.join(self.listCapabilities()), self.ID
ENT) | |
872 self.sendPositiveResponse(message=msg) | |
873 | |
874 def sendBadResponse(self, tag = None, message = ''): | |
875 self._respond('BAD', tag, message) | |
876 | |
877 def sendPositiveResponse(self, tag = None, message = ''): | |
878 self._respond('OK', tag, message) | |
879 | |
880 def sendNegativeResponse(self, tag = None, message = ''): | |
881 self._respond('NO', tag, message) | |
882 | |
883 def sendUntaggedResponse(self, message, async=False): | |
884 if not async or (self.blocked is None): | |
885 self._respond(message, None, None) | |
886 else: | |
887 self._queuedAsync.append(message) | |
888 | |
889 def sendContinuationRequest(self, msg = 'Ready for additional command text')
: | |
890 if msg: | |
891 self.sendLine('+ ' + msg) | |
892 else: | |
893 self.sendLine('+') | |
894 | |
895 def _respond(self, state, tag, message): | |
896 if state in ('OK', 'NO', 'BAD') and self._queuedAsync: | |
897 lines = self._queuedAsync | |
898 self._queuedAsync = [] | |
899 for msg in lines: | |
900 self._respond(msg, None, None) | |
901 if not tag: | |
902 tag = '*' | |
903 if message: | |
904 self.sendLine(' '.join((tag, state, message))) | |
905 else: | |
906 self.sendLine(' '.join((tag, state))) | |
907 | |
908 def listCapabilities(self): | |
909 caps = ['IMAP4rev1'] | |
910 for c, v in self.capabilities().iteritems(): | |
911 if v is None: | |
912 caps.append(c) | |
913 elif len(v): | |
914 caps.extend([('%s=%s' % (c, cap)) for cap in v]) | |
915 return caps | |
916 | |
917 def do_CAPABILITY(self, tag): | |
918 self.sendUntaggedResponse('CAPABILITY ' + ' '.join(self.listCapabilities
())) | |
919 self.sendPositiveResponse(tag, 'CAPABILITY completed') | |
920 | |
921 unauth_CAPABILITY = (do_CAPABILITY,) | |
922 auth_CAPABILITY = unauth_CAPABILITY | |
923 select_CAPABILITY = unauth_CAPABILITY | |
924 logout_CAPABILITY = unauth_CAPABILITY | |
925 | |
926 def do_LOGOUT(self, tag): | |
927 self.sendUntaggedResponse('BYE Nice talking to you') | |
928 self.sendPositiveResponse(tag, 'LOGOUT successful') | |
929 self.transport.loseConnection() | |
930 | |
931 unauth_LOGOUT = (do_LOGOUT,) | |
932 auth_LOGOUT = unauth_LOGOUT | |
933 select_LOGOUT = unauth_LOGOUT | |
934 logout_LOGOUT = unauth_LOGOUT | |
935 | |
936 def do_NOOP(self, tag): | |
937 self.sendPositiveResponse(tag, 'NOOP No operation performed') | |
938 | |
939 unauth_NOOP = (do_NOOP,) | |
940 auth_NOOP = unauth_NOOP | |
941 select_NOOP = unauth_NOOP | |
942 logout_NOOP = unauth_NOOP | |
943 | |
944 def do_AUTHENTICATE(self, tag, args): | |
945 args = args.upper().strip() | |
946 if args not in self.challengers: | |
947 self.sendNegativeResponse(tag, 'AUTHENTICATE method unsupported') | |
948 else: | |
949 self.authenticate(self.challengers[args](), tag) | |
950 | |
951 unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom) | |
952 | |
953 def authenticate(self, chal, tag): | |
954 if self.portal is None: | |
955 self.sendNegativeResponse(tag, 'Temporary authentication failure') | |
956 return | |
957 | |
958 self._setupChallenge(chal, tag) | |
959 | |
960 def _setupChallenge(self, chal, tag): | |
961 try: | |
962 challenge = chal.getChallenge() | |
963 except Exception, e: | |
964 self.sendBadResponse(tag, 'Server error: ' + str(e)) | |
965 else: | |
966 coded = base64.encodestring(challenge)[:-1] | |
967 self.parseState = 'pending' | |
968 self._pendingLiteral = defer.Deferred() | |
969 self.sendContinuationRequest(coded) | |
970 self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag) | |
971 self._pendingLiteral.addErrback(self.__ebAuthChunk, tag) | |
972 | |
973 def __cbAuthChunk(self, result, chal, tag): | |
974 try: | |
975 uncoded = base64.decodestring(result) | |
976 except binascii.Error: | |
977 raise IllegalClientResponse("Malformed Response - not base64") | |
978 | |
979 chal.setResponse(uncoded) | |
980 if chal.moreChallenges(): | |
981 self._setupChallenge(chal, tag) | |
982 else: | |
983 self.portal.login(chal, None, IAccount).addCallbacks( | |
984 self.__cbAuthResp, | |
985 self.__ebAuthResp, | |
986 (tag,), None, (tag,), None | |
987 ) | |
988 | |
989 def __cbAuthResp(self, (iface, avatar, logout), tag): | |
990 assert iface is IAccount, "IAccount is the only supported interface" | |
991 self.account = avatar | |
992 self.state = 'auth' | |
993 self._onLogout = logout | |
994 self.sendPositiveResponse(tag, 'Authentication successful') | |
995 self.setTimeout(self.POSTAUTH_TIMEOUT) | |
996 | |
997 def __ebAuthResp(self, failure, tag): | |
998 if failure.check(cred.error.UnauthorizedLogin): | |
999 self.sendNegativeResponse(tag, 'Authentication failed: unauthorized'
) | |
1000 elif failure.check(cred.error.UnhandledCredentials): | |
1001 self.sendNegativeResponse(tag, 'Authentication failed: server miscon
figured') | |
1002 else: | |
1003 self.sendBadResponse(tag, 'Server error: login failed unexpectedly') | |
1004 log.err(failure) | |
1005 | |
1006 def __ebAuthChunk(self, failure, tag): | |
1007 self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.v
alue)) | |
1008 | |
1009 def do_STARTTLS(self, tag): | |
1010 if self.startedTLS: | |
1011 self.sendNegativeResponse(tag, 'TLS already negotiated') | |
1012 elif self.ctx and self.canStartTLS: | |
1013 self.sendPositiveResponse(tag, 'Begin TLS negotiation now') | |
1014 self.transport.startTLS(self.ctx) | |
1015 self.startedTLS = True | |
1016 self.challengers = self.challengers.copy() | |
1017 if 'LOGIN' not in self.challengers: | |
1018 self.challengers['LOGIN'] = LOGINCredentials | |
1019 if 'PLAIN' not in self.challengers: | |
1020 self.challengers['PLAIN'] = PLAINCredentials | |
1021 else: | |
1022 self.sendNegativeResponse(tag, 'TLS not available') | |
1023 | |
1024 unauth_STARTTLS = (do_STARTTLS,) | |
1025 | |
1026 def do_LOGIN(self, tag, user, passwd): | |
1027 if 'LOGINDISABLED' in self.capabilities(): | |
1028 self.sendBadResponse(tag, 'LOGIN is disabled before STARTTLS') | |
1029 return | |
1030 | |
1031 maybeDeferred(self.authenticateLogin, user, passwd | |
1032 ).addCallback(self.__cbLogin, tag | |
1033 ).addErrback(self.__ebLogin, tag | |
1034 ) | |
1035 | |
1036 unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring) | |
1037 | |
1038 def authenticateLogin(self, user, passwd): | |
1039 """Lookup the account associated with the given parameters | |
1040 | |
1041 Override this method to define the desired authentication behavior. | |
1042 | |
1043 The default behavior is to defer authentication to C{self.portal} | |
1044 if it is not None, or to deny the login otherwise. | |
1045 | |
1046 @type user: C{str} | |
1047 @param user: The username to lookup | |
1048 | |
1049 @type passwd: C{str} | |
1050 @param passwd: The password to login with | |
1051 """ | |
1052 if self.portal: | |
1053 return self.portal.login( | |
1054 cred.credentials.UsernamePassword(user, passwd), | |
1055 None, IAccount | |
1056 ) | |
1057 raise cred.error.UnauthorizedLogin() | |
1058 | |
1059 def __cbLogin(self, (iface, avatar, logout), tag): | |
1060 if iface is not IAccount: | |
1061 self.sendBadResponse(tag, 'Server error: login returned unexpected v
alue') | |
1062 log.err("__cbLogin called with %r, IAccount expected" % (iface,)) | |
1063 else: | |
1064 self.account = avatar | |
1065 self._onLogout = logout | |
1066 self.sendPositiveResponse(tag, 'LOGIN succeeded') | |
1067 self.state = 'auth' | |
1068 self.setTimeout(self.POSTAUTH_TIMEOUT) | |
1069 | |
1070 def __ebLogin(self, failure, tag): | |
1071 if failure.check(cred.error.UnauthorizedLogin): | |
1072 self.sendNegativeResponse(tag, 'LOGIN failed') | |
1073 else: | |
1074 self.sendBadResponse(tag, 'Server error: ' + str(failure.value)) | |
1075 log.err(failure) | |
1076 | |
1077 def do_NAMESPACE(self, tag): | |
1078 personal = public = shared = None | |
1079 np = INamespacePresenter(self.account, None) | |
1080 if np is not None: | |
1081 personal = np.getPersonalNamespaces() | |
1082 public = np.getSharedNamespaces() | |
1083 shared = np.getSharedNamespaces() | |
1084 self.sendUntaggedResponse('NAMESPACE ' + collapseNestedLists([personal,
public, shared])) | |
1085 self.sendPositiveResponse(tag, "NAMESPACE command completed") | |
1086 | |
1087 auth_NAMESPACE = (do_NAMESPACE,) | |
1088 select_NAMESPACE = auth_NAMESPACE | |
1089 | |
1090 def _parseMbox(self, name): | |
1091 if isinstance(name, unicode): | |
1092 return name | |
1093 try: | |
1094 return name.decode('imap4-utf-7') | |
1095 except: | |
1096 log.err() | |
1097 raise IllegalMailboxEncoding(name) | |
1098 | |
1099 def _selectWork(self, tag, name, rw, cmdName): | |
1100 if self.mbox: | |
1101 self.mbox.removeListener(self) | |
1102 cmbx = ICloseableMailbox(self.mbox, None) | |
1103 if cmbx is not None: | |
1104 maybeDeferred(cmbx.close).addErrback(log.err) | |
1105 self.mbox = None | |
1106 self.state = 'auth' | |
1107 | |
1108 name = self._parseMbox(name) | |
1109 maybeDeferred(self.account.select, self._parseMbox(name), rw | |
1110 ).addCallback(self._cbSelectWork, cmdName, tag | |
1111 ).addErrback(self._ebSelectWork, cmdName, tag | |
1112 ) | |
1113 | |
1114 def _ebSelectWork(self, failure, cmdName, tag): | |
1115 self.sendBadResponse(tag, "%s failed: Server error" % (cmdName,)) | |
1116 log.err(failure) | |
1117 | |
1118 def _cbSelectWork(self, mbox, cmdName, tag): | |
1119 if mbox is None: | |
1120 self.sendNegativeResponse(tag, 'No such mailbox') | |
1121 return | |
1122 if '\\noselect' in [s.lower() for s in mbox.getFlags()]: | |
1123 self.sendNegativeResponse(tag, 'Mailbox cannot be selected') | |
1124 return | |
1125 | |
1126 flags = mbox.getFlags() | |
1127 self.sendUntaggedResponse(str(mbox.getMessageCount()) + ' EXISTS') | |
1128 self.sendUntaggedResponse(str(mbox.getRecentCount()) + ' RECENT') | |
1129 self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags)) | |
1130 self.sendPositiveResponse(None, '[UIDVALIDITY %d]' % mbox.getUIDValidity
()) | |
1131 | |
1132 s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY' | |
1133 mbox.addListener(self) | |
1134 self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName)) | |
1135 self.state = 'select' | |
1136 self.mbox = mbox | |
1137 | |
1138 auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' ) | |
1139 select_SELECT = auth_SELECT | |
1140 | |
1141 auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' ) | |
1142 select_EXAMINE = auth_EXAMINE | |
1143 | |
1144 | |
1145 def do_IDLE(self, tag): | |
1146 self.sendContinuationRequest(None) | |
1147 self.parseTag = tag | |
1148 self.lastState = self.parseState | |
1149 self.parseState = 'idle' | |
1150 | |
1151 def parse_idle(self, *args): | |
1152 self.parseState = self.lastState | |
1153 del self.lastState | |
1154 self.sendPositiveResponse(self.parseTag, "IDLE terminated") | |
1155 del self.parseTag | |
1156 | |
1157 select_IDLE = ( do_IDLE, ) | |
1158 auth_IDLE = select_IDLE | |
1159 | |
1160 | |
1161 def do_CREATE(self, tag, name): | |
1162 name = self._parseMbox(name) | |
1163 try: | |
1164 result = self.account.create(name) | |
1165 except MailboxException, c: | |
1166 self.sendNegativeResponse(tag, str(c)) | |
1167 except: | |
1168 self.sendBadResponse(tag, "Server error encountered while creating m
ailbox") | |
1169 log.err() | |
1170 else: | |
1171 if result: | |
1172 self.sendPositiveResponse(tag, 'Mailbox created') | |
1173 else: | |
1174 self.sendNegativeResponse(tag, 'Mailbox not created') | |
1175 | |
1176 auth_CREATE = (do_CREATE, arg_astring) | |
1177 select_CREATE = auth_CREATE | |
1178 | |
1179 def do_DELETE(self, tag, name): | |
1180 name = self._parseMbox(name) | |
1181 if name.lower() == 'inbox': | |
1182 self.sendNegativeResponse(tag, 'You cannot delete the inbox') | |
1183 return | |
1184 try: | |
1185 self.account.delete(name) | |
1186 except MailboxException, m: | |
1187 self.sendNegativeResponse(tag, str(m)) | |
1188 except: | |
1189 self.sendBadResponse(tag, "Server error encountered while deleting m
ailbox") | |
1190 log.err() | |
1191 else: | |
1192 self.sendPositiveResponse(tag, 'Mailbox deleted') | |
1193 | |
1194 auth_DELETE = (do_DELETE, arg_astring) | |
1195 select_DELETE = auth_DELETE | |
1196 | |
1197 def do_RENAME(self, tag, oldname, newname): | |
1198 oldname, newname = [self._parseMbox(n) for n in oldname, newname] | |
1199 if oldname.lower() == 'inbox' or newname.lower() == 'inbox': | |
1200 self.sendNegativeResponse(tag, 'You cannot rename the inbox, or rena
me another mailbox to inbox.') | |
1201 return | |
1202 try: | |
1203 self.account.rename(oldname, newname) | |
1204 except TypeError: | |
1205 self.sendBadResponse(tag, 'Invalid command syntax') | |
1206 except MailboxException, m: | |
1207 self.sendNegativeResponse(tag, str(m)) | |
1208 except: | |
1209 self.sendBadResponse(tag, "Server error encountered while renaming m
ailbox") | |
1210 log.err() | |
1211 else: | |
1212 self.sendPositiveResponse(tag, 'Mailbox renamed') | |
1213 | |
1214 auth_RENAME = (do_RENAME, arg_astring, arg_astring) | |
1215 select_RENAME = auth_RENAME | |
1216 | |
1217 def do_SUBSCRIBE(self, tag, name): | |
1218 name = self._parseMbox(name) | |
1219 try: | |
1220 self.account.subscribe(name) | |
1221 except MailboxException, m: | |
1222 self.sendNegativeResponse(tag, str(m)) | |
1223 except: | |
1224 self.sendBadResponse(tag, "Server error encountered while subscribin
g to mailbox") | |
1225 log.err() | |
1226 else: | |
1227 self.sendPositiveResponse(tag, 'Subscribed') | |
1228 | |
1229 auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring) | |
1230 select_SUBSCRIBE = auth_SUBSCRIBE | |
1231 | |
1232 def do_UNSUBSCRIBE(self, tag, name): | |
1233 name = self._parseMbox(name) | |
1234 try: | |
1235 self.account.unsubscribe(name) | |
1236 except MailboxException, m: | |
1237 self.sendNegativeResponse(tag, str(m)) | |
1238 except: | |
1239 self.sendBadResponse(tag, "Server error encountered while unsubscrib
ing from mailbox") | |
1240 log.err() | |
1241 else: | |
1242 self.sendPositiveResponse(tag, 'Unsubscribed') | |
1243 | |
1244 auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring) | |
1245 select_UNSUBSCRIBE = auth_UNSUBSCRIBE | |
1246 | |
1247 def _listWork(self, tag, ref, mbox, sub, cmdName): | |
1248 mbox = self._parseMbox(mbox) | |
1249 maybeDeferred(self.account.listMailboxes, ref, mbox | |
1250 ).addCallback(self._cbListWork, tag, sub, cmdName | |
1251 ).addErrback(self._ebListWork, tag | |
1252 ) | |
1253 | |
1254 def _cbListWork(self, mailboxes, tag, sub, cmdName): | |
1255 for (name, box) in mailboxes: | |
1256 if not sub or self.account.isSubscribed(name): | |
1257 flags = box.getFlags() | |
1258 delim = box.getHierarchicalDelimiter() | |
1259 resp = (DontQuoteMe(cmdName), map(DontQuoteMe, flags), delim, na
me.encode('imap4-utf-7')) | |
1260 self.sendUntaggedResponse(collapseNestedLists(resp)) | |
1261 self.sendPositiveResponse(tag, '%s completed' % (cmdName,)) | |
1262 | |
1263 def _ebListWork(self, failure, tag): | |
1264 self.sendBadResponse(tag, "Server error encountered while listing mailbo
xes.") | |
1265 log.err(failure) | |
1266 | |
1267 auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST') | |
1268 select_LIST = auth_LIST | |
1269 | |
1270 auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB') | |
1271 select_LSUB = auth_LSUB | |
1272 | |
1273 def do_STATUS(self, tag, mailbox, names): | |
1274 mailbox = self._parseMbox(mailbox) | |
1275 maybeDeferred(self.account.select, mailbox, 0 | |
1276 ).addCallback(self._cbStatusGotMailbox, tag, mailbox, names | |
1277 ).addErrback(self._ebStatusGotMailbox, tag | |
1278 ) | |
1279 | |
1280 def _cbStatusGotMailbox(self, mbox, tag, mailbox, names): | |
1281 if mbox: | |
1282 maybeDeferred(mbox.requestStatus, names).addCallbacks( | |
1283 self.__cbStatus, self.__ebStatus, | |
1284 (tag, mailbox), None, (tag, mailbox), None | |
1285 ) | |
1286 else: | |
1287 self.sendNegativeResponse(tag, "Could not open mailbox") | |
1288 | |
1289 def _ebStatusGotMailbox(self, failure, tag): | |
1290 self.sendBadResponse(tag, "Server error encountered while opening mailbo
x.") | |
1291 log.err(failure) | |
1292 | |
1293 auth_STATUS = (do_STATUS, arg_astring, arg_plist) | |
1294 select_STATUS = auth_STATUS | |
1295 | |
1296 def __cbStatus(self, status, tag, box): | |
1297 line = ' '.join(['%s %s' % x for x in status.iteritems()]) | |
1298 self.sendUntaggedResponse('STATUS %s (%s)' % (box, line)) | |
1299 self.sendPositiveResponse(tag, 'STATUS complete') | |
1300 | |
1301 def __ebStatus(self, failure, tag, box): | |
1302 self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, str(failure.val
ue))) | |
1303 | |
1304 def do_APPEND(self, tag, mailbox, flags, date, message): | |
1305 mailbox = self._parseMbox(mailbox) | |
1306 maybeDeferred(self.account.select, mailbox | |
1307 ).addCallback(self._cbAppendGotMailbox, tag, flags, date, message | |
1308 ).addErrback(self._ebAppendGotMailbox, tag | |
1309 ) | |
1310 | |
1311 def _cbAppendGotMailbox(self, mbox, tag, flags, date, message): | |
1312 if not mbox: | |
1313 self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox') | |
1314 return | |
1315 | |
1316 d = mbox.addMessage(message, flags, date) | |
1317 d.addCallback(self.__cbAppend, tag, mbox) | |
1318 d.addErrback(self.__ebAppend, tag) | |
1319 | |
1320 def _ebAppendGotMailbox(self, failure, tag): | |
1321 self.sendBadResponse(tag, "Server error encountered while opening mailbo
x.") | |
1322 log.err(failure) | |
1323 | |
1324 auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime, | |
1325 arg_literal) | |
1326 select_APPEND = auth_APPEND | |
1327 | |
1328 def __cbAppend(self, result, tag, mbox): | |
1329 self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount()) | |
1330 self.sendPositiveResponse(tag, 'APPEND complete') | |
1331 | |
1332 def __ebAppend(self, failure, tag): | |
1333 self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value)) | |
1334 | |
1335 def do_CHECK(self, tag): | |
1336 d = self.checkpoint() | |
1337 if d is None: | |
1338 self.__cbCheck(None, tag) | |
1339 else: | |
1340 d.addCallbacks( | |
1341 self.__cbCheck, | |
1342 self.__ebCheck, | |
1343 callbackArgs=(tag,), | |
1344 errbackArgs=(tag,) | |
1345 ) | |
1346 select_CHECK = (do_CHECK,) | |
1347 | |
1348 def __cbCheck(self, result, tag): | |
1349 self.sendPositiveResponse(tag, 'CHECK completed') | |
1350 | |
1351 def __ebCheck(self, failure, tag): | |
1352 self.sendBadResponse(tag, 'CHECK failed: ' + str(failure.value)) | |
1353 | |
1354 def checkpoint(self): | |
1355 """Called when the client issues a CHECK command. | |
1356 | |
1357 This should perform any checkpoint operations required by the server. | |
1358 It may be a long running operation, but may not block. If it returns | |
1359 a deferred, the client will only be informed of success (or failure) | |
1360 when the deferred's callback (or errback) is invoked. | |
1361 """ | |
1362 return None | |
1363 | |
1364 def do_CLOSE(self, tag): | |
1365 d = None | |
1366 if self.mbox.isWriteable(): | |
1367 d = maybeDeferred(self.mbox.expunge) | |
1368 cmbx = ICloseableMailbox(self.mbox, None) | |
1369 if cmbx is not None: | |
1370 if d is not None: | |
1371 d.addCallback(lambda result: cmbx.close()) | |
1372 else: | |
1373 d = maybeDeferred(cmbx.close) | |
1374 if d is not None: | |
1375 d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,),
None) | |
1376 else: | |
1377 self.__cbClose(None, tag) | |
1378 | |
1379 select_CLOSE = (do_CLOSE,) | |
1380 | |
1381 def __cbClose(self, result, tag): | |
1382 self.sendPositiveResponse(tag, 'CLOSE completed') | |
1383 self.mbox.removeListener(self) | |
1384 self.mbox = None | |
1385 self.state = 'auth' | |
1386 | |
1387 def __ebClose(self, failure, tag): | |
1388 self.sendBadResponse(tag, 'CLOSE failed: ' + str(failure.value)) | |
1389 | |
1390 def do_EXPUNGE(self, tag): | |
1391 if self.mbox.isWriteable(): | |
1392 maybeDeferred(self.mbox.expunge).addCallbacks( | |
1393 self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None | |
1394 ) | |
1395 else: | |
1396 self.sendNegativeResponse(tag, 'EXPUNGE ignored on read-only mailbox
') | |
1397 | |
1398 select_EXPUNGE = (do_EXPUNGE,) | |
1399 | |
1400 def __cbExpunge(self, result, tag): | |
1401 for e in result: | |
1402 self.sendUntaggedResponse('%d EXPUNGE' % e) | |
1403 self.sendPositiveResponse(tag, 'EXPUNGE completed') | |
1404 | |
1405 def __ebExpunge(self, failure, tag): | |
1406 self.sendBadResponse(tag, 'EXPUNGE failed: ' + str(failure.value)) | |
1407 log.err(failure) | |
1408 | |
1409 def do_SEARCH(self, tag, charset, query, uid=0): | |
1410 sm = ISearchableMailbox(self.mbox, None) | |
1411 if sm is not None: | |
1412 maybeDeferred(sm.search, query, uid=uid).addCallbacks( | |
1413 self.__cbSearch, self.__ebSearch, | |
1414 (tag, self.mbox, uid), None, (tag,), None | |
1415 ) | |
1416 else: | |
1417 s = parseIdList('1:*') | |
1418 maybeDeferred(self.mbox.fetch, s, uid=uid).addCallbacks( | |
1419 self.__cbManualSearch, self.__ebSearch, | |
1420 (tag, self.mbox, query, uid), None, (tag,), None | |
1421 ) | |
1422 | |
1423 select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys) | |
1424 | |
1425 def __cbSearch(self, result, tag, mbox, uid): | |
1426 if uid: | |
1427 result = map(mbox.getUID, result) | |
1428 ids = ' '.join([str(i) for i in result]) | |
1429 self.sendUntaggedResponse('SEARCH ' + ids) | |
1430 self.sendPositiveResponse(tag, 'SEARCH completed') | |
1431 | |
1432 def __cbManualSearch(self, result, tag, mbox, query, uid, searchResults = No
ne): | |
1433 if searchResults is None: | |
1434 searchResults = [] | |
1435 i = 0 | |
1436 for (i, (id, msg)) in zip(range(5), result): | |
1437 if self.searchFilter(query, id, msg): | |
1438 if uid: | |
1439 searchResults.append(str(msg.getUID())) | |
1440 else: | |
1441 searchResults.append(str(id)) | |
1442 if i == 4: | |
1443 from twisted.internet import reactor | |
1444 reactor.callLater(0, self.__cbManualSearch, result, tag, mbox, query
, uid, searchResults) | |
1445 else: | |
1446 if searchResults: | |
1447 self.sendUntaggedResponse('SEARCH ' + ' '.join(searchResults)) | |
1448 self.sendPositiveResponse(tag, 'SEARCH completed') | |
1449 | |
1450 def searchFilter(self, query, id, msg): | |
1451 while query: | |
1452 if not self.singleSearchStep(query, id, msg): | |
1453 return False | |
1454 return True | |
1455 | |
1456 def singleSearchStep(self, query, id, msg): | |
1457 q = query.pop(0) | |
1458 if isinstance(q, list): | |
1459 if not self.searchFilter(q, id, msg): | |
1460 return False | |
1461 else: | |
1462 c = q.upper() | |
1463 f = getattr(self, 'search_' + c) | |
1464 if f: | |
1465 if not f(query, id, msg): | |
1466 return False | |
1467 else: | |
1468 # IMAP goes *out of its way* to be complex | |
1469 # Sequence sets to search should be specified | |
1470 # with a command, like EVERYTHING ELSE. | |
1471 try: | |
1472 m = parseIdList(c) | |
1473 except: | |
1474 log.err('Unknown search term: ' + c) | |
1475 else: | |
1476 if id not in m: | |
1477 return False | |
1478 return True | |
1479 | |
1480 def search_ALL(self, query, id, msg): | |
1481 return True | |
1482 | |
1483 def search_ANSWERED(self, query, id, msg): | |
1484 return '\\Answered' in msg.getFlags() | |
1485 | |
1486 def search_BCC(self, query, id, msg): | |
1487 bcc = msg.getHeaders(False, 'bcc').get('bcc', '') | |
1488 return bcc.lower().find(query.pop(0).lower()) != -1 | |
1489 | |
1490 def search_BEFORE(self, query, id, msg): | |
1491 date = parseTime(query.pop(0)) | |
1492 return rfc822.parsedate(msg.getInternalDate()) < date | |
1493 | |
1494 def search_BODY(self, query, id, msg): | |
1495 body = query.pop(0).lower() | |
1496 return text.strFile(body, msg.getBodyFile(), False) | |
1497 | |
1498 def search_CC(self, query, id, msg): | |
1499 cc = msg.getHeaders(False, 'cc').get('cc', '') | |
1500 return cc.lower().find(query.pop(0).lower()) != -1 | |
1501 | |
1502 def search_DELETED(self, query, id, msg): | |
1503 return '\\Deleted' in msg.getFlags() | |
1504 | |
1505 def search_DRAFT(self, query, id, msg): | |
1506 return '\\Draft' in msg.getFlags() | |
1507 | |
1508 def search_FLAGGED(self, query, id, msg): | |
1509 return '\\Flagged' in msg.getFlags() | |
1510 | |
1511 def search_FROM(self, query, id, msg): | |
1512 fm = msg.getHeaders(False, 'from').get('from', '') | |
1513 return fm.lower().find(query.pop(0).lower()) != -1 | |
1514 | |
1515 def search_HEADER(self, query, id, msg): | |
1516 hdr = query.pop(0).lower() | |
1517 hdr = msg.getHeaders(False, hdr).get(hdr, '') | |
1518 return hdr.lower().find(query.pop(0).lower()) != -1 | |
1519 | |
1520 def search_KEYWORD(self, query, id, msg): | |
1521 query.pop(0) | |
1522 return False | |
1523 | |
1524 def search_LARGER(self, query, id, msg): | |
1525 return int(query.pop(0)) < msg.getSize() | |
1526 | |
1527 def search_NEW(self, query, id, msg): | |
1528 return '\\Recent' in msg.getFlags() and '\\Seen' not in msg.getFlags() | |
1529 | |
1530 def search_NOT(self, query, id, msg): | |
1531 return not self.singleSearchStep(query, id, msg) | |
1532 | |
1533 def search_OLD(self, query, id, msg): | |
1534 return '\\Recent' not in msg.getFlags() | |
1535 | |
1536 def search_ON(self, query, id, msg): | |
1537 date = parseTime(query.pop(0)) | |
1538 return rfc822.parsedate(msg.getInternalDate()) == date | |
1539 | |
1540 def search_OR(self, query, id, msg): | |
1541 a = self.singleSearchStep(query, id, msg) | |
1542 b = self.singleSearchStep(query, id, msg) | |
1543 return a or b | |
1544 | |
1545 def search_RECENT(self, query, id, msg): | |
1546 return '\\Recent' in msg.getFlags() | |
1547 | |
1548 def search_SEEN(self, query, id, msg): | |
1549 return '\\Seen' in msg.getFlags() | |
1550 | |
1551 def search_SENTBEFORE(self, query, id, msg): | |
1552 date = msg.getHeader(False, 'date').get('date', '') | |
1553 date = rfc822.parsedate(date) | |
1554 return date < parseTime(query.pop(0)) | |
1555 | |
1556 def search_SENTON(self, query, id, msg): | |
1557 date = msg.getHeader(False, 'date').get('date', '') | |
1558 date = rfc822.parsedate(date) | |
1559 return date[:3] == parseTime(query.pop(0))[:3] | |
1560 | |
1561 def search_SENTSINCE(self, query, id, msg): | |
1562 date = msg.getHeader(False, 'date').get('date', '') | |
1563 date = rfc822.parsedate(date) | |
1564 return date > parseTime(query.pop(0)) | |
1565 | |
1566 def search_SINCE(self, query, id, msg): | |
1567 date = parseTime(query.pop(0)) | |
1568 return rfc822.parsedate(msg.getInternalDate()) > date | |
1569 | |
1570 def search_SMALLER(self, query, id, msg): | |
1571 return int(query.pop(0)) > msg.getSize() | |
1572 | |
1573 def search_SUBJECT(self, query, id, msg): | |
1574 subj = msg.getHeaders(False, 'subject').get('subject', '') | |
1575 return subj.lower().find(query.pop(0).lower()) != -1 | |
1576 | |
1577 def search_TEXT(self, query, id, msg): | |
1578 # XXX - This must search headers too | |
1579 body = query.pop(0).lower() | |
1580 return text.strFile(body, msg.getBodyFile(), False) | |
1581 | |
1582 def search_TO(self, query, id, msg): | |
1583 to = msg.getHeaders(False, 'to').get('to', '') | |
1584 return to.lower().find(query.pop(0).lower()) != -1 | |
1585 | |
1586 def search_UID(self, query, id, msg): | |
1587 c = query.pop(0) | |
1588 m = parseIdList(c) | |
1589 return msg.getUID() in m | |
1590 | |
1591 def search_UNANSWERED(self, query, id, msg): | |
1592 return '\\Answered' not in msg.getFlags() | |
1593 | |
1594 def search_UNDELETED(self, query, id, msg): | |
1595 return '\\Deleted' not in msg.getFlags() | |
1596 | |
1597 def search_UNDRAFT(self, query, id, msg): | |
1598 return '\\Draft' not in msg.getFlags() | |
1599 | |
1600 def search_UNFLAGGED(self, query, id, msg): | |
1601 return '\\Flagged' not in msg.getFlags() | |
1602 | |
1603 def search_UNKEYWORD(self, query, id, msg): | |
1604 query.pop(0) | |
1605 return False | |
1606 | |
1607 def search_UNSEEN(self, query, id, msg): | |
1608 return '\\Seen' not in msg.getFlags() | |
1609 | |
1610 def __ebSearch(self, failure, tag): | |
1611 self.sendBadResponse(tag, 'SEARCH failed: ' + str(failure.value)) | |
1612 log.err(failure) | |
1613 | |
1614 def do_FETCH(self, tag, messages, query, uid=0): | |
1615 if query: | |
1616 self._oldTimeout = self.setTimeout(None) | |
1617 maybeDeferred(self.mbox.fetch, messages, uid=uid | |
1618 ).addCallback(iter | |
1619 ).addCallback(self.__cbFetch, tag, query, uid | |
1620 ).addErrback(self.__ebFetch, tag | |
1621 ) | |
1622 else: | |
1623 self.sendPositiveResponse(tag, 'FETCH complete') | |
1624 | |
1625 select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt) | |
1626 | |
1627 def __cbFetch(self, results, tag, query, uid): | |
1628 if self.blocked is None: | |
1629 self.blocked = [] | |
1630 try: | |
1631 id, msg = results.next() | |
1632 except StopIteration: | |
1633 # The idle timeout was suspended while we delivered results, | |
1634 # restore it now. | |
1635 self.setTimeout(self._oldTimeout) | |
1636 del self._oldTimeout | |
1637 | |
1638 # All results have been processed, deliver completion notification. | |
1639 | |
1640 # It's important to run this *after* resetting the timeout to "rig | |
1641 # a race" in some test code. writing to the transport will | |
1642 # synchronously call test code, which synchronously loses the | |
1643 # connection, calling our connectionLost method, which cancels the | |
1644 # timeout. We want to make sure that timeout is cancelled *after* | |
1645 # we reset it above, so that the final state is no timed | |
1646 # calls. This avoids reactor uncleanliness errors in the test | |
1647 # suite. | |
1648 # XXX: Perhaps loopback should be fixed to not call the user code | |
1649 # synchronously in transport.write? | |
1650 self.sendPositiveResponse(tag, 'FETCH completed') | |
1651 | |
1652 # Instance state is now consistent again (ie, it is as though | |
1653 # the fetch command never ran), so allow any pending blocked | |
1654 # commands to execute. | |
1655 self._unblock() | |
1656 else: | |
1657 self.spewMessage(id, msg, query, uid | |
1658 ).addCallback(lambda _: self.__cbFetch(results, tag, query, uid) | |
1659 ).addErrback(self.__ebSpewMessage | |
1660 ) | |
1661 | |
1662 def __ebSpewMessage(self, failure): | |
1663 # This indicates a programming error. | |
1664 # There's no reliable way to indicate anything to the client, since we | |
1665 # may have already written an arbitrary amount of data in response to | |
1666 # the command. | |
1667 log.err(failure) | |
1668 self.transport.loseConnection() | |
1669 | |
1670 def spew_envelope(self, id, msg, _w=None, _f=None): | |
1671 if _w is None: | |
1672 _w = self.transport.write | |
1673 _w('ENVELOPE ' + collapseNestedLists([getEnvelope(msg)])) | |
1674 | |
1675 def spew_flags(self, id, msg, _w=None, _f=None): | |
1676 if _w is None: | |
1677 _w = self.transport.write | |
1678 _w('FLAGS ' + '(%s)' % (' '.join(msg.getFlags()))) | |
1679 | |
1680 def spew_internaldate(self, id, msg, _w=None, _f=None): | |
1681 if _w is None: | |
1682 _w = self.transport.write | |
1683 idate = msg.getInternalDate() | |
1684 ttup = rfc822.parsedate_tz(idate) | |
1685 if ttup is None: | |
1686 log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate)) | |
1687 raise IMAP4Exception("Internal failure generating INTERNALDATE") | |
1688 | |
1689 odate = time.strftime("%d-%b-%Y %H:%M:%S ", ttup[:9]) | |
1690 if ttup[9] is None: | |
1691 odate = odate + "+0000" | |
1692 else: | |
1693 if ttup[9] >= 0: | |
1694 sign = "+" | |
1695 else: | |
1696 sign = "-" | |
1697 odate = odate + sign + string.zfill(str(((abs(ttup[9]) / 3600) * 100
+ (abs(ttup[9]) % 3600) / 60)), 4) | |
1698 _w('INTERNALDATE ' + _quote(odate)) | |
1699 | |
1700 def spew_rfc822header(self, id, msg, _w=None, _f=None): | |
1701 if _w is None: | |
1702 _w = self.transport.write | |
1703 hdrs = _formatHeaders(msg.getHeaders(True)) | |
1704 _w('RFC822.HEADER ' + _literal(hdrs)) | |
1705 | |
1706 def spew_rfc822text(self, id, msg, _w=None, _f=None): | |
1707 if _w is None: | |
1708 _w = self.transport.write | |
1709 _w('RFC822.TEXT ') | |
1710 _f() | |
1711 return FileProducer(msg.getBodyFile() | |
1712 ).beginProducing(self.transport | |
1713 ) | |
1714 | |
1715 def spew_rfc822size(self, id, msg, _w=None, _f=None): | |
1716 if _w is None: | |
1717 _w = self.transport.write | |
1718 _w('RFC822.SIZE ' + str(msg.getSize())) | |
1719 | |
1720 def spew_rfc822(self, id, msg, _w=None, _f=None): | |
1721 if _w is None: | |
1722 _w = self.transport.write | |
1723 _w('RFC822 ') | |
1724 _f() | |
1725 mf = IMessageFile(msg, None) | |
1726 if mf is not None: | |
1727 return FileProducer(mf.open() | |
1728 ).beginProducing(self.transport | |
1729 ) | |
1730 return MessageProducer(msg, None, self._scheduler | |
1731 ).beginProducing(self.transport | |
1732 ) | |
1733 | |
1734 def spew_uid(self, id, msg, _w=None, _f=None): | |
1735 if _w is None: | |
1736 _w = self.transport.write | |
1737 _w('UID ' + str(msg.getUID())) | |
1738 | |
1739 def spew_bodystructure(self, id, msg, _w=None, _f=None): | |
1740 _w('BODYSTRUCTURE ' + collapseNestedLists([getBodyStructure(msg, True)])
) | |
1741 | |
1742 def spew_body(self, part, id, msg, _w=None, _f=None): | |
1743 if _w is None: | |
1744 _w = self.transport.write | |
1745 for p in part.part: | |
1746 if msg.isMultipart(): | |
1747 msg = msg.getSubPart(p) | |
1748 elif p > 0: | |
1749 # Non-multipart messages have an implicit first part but no | |
1750 # other parts - reject any request for any other part. | |
1751 raise TypeError("Requested subpart of non-multipart message") | |
1752 | |
1753 if part.header: | |
1754 hdrs = msg.getHeaders(part.header.negate, *part.header.fields) | |
1755 hdrs = _formatHeaders(hdrs) | |
1756 _w(str(part) + ' ' + _literal(hdrs)) | |
1757 elif part.text: | |
1758 _w(str(part) + ' ') | |
1759 _f() | |
1760 return FileProducer(msg.getBodyFile() | |
1761 ).beginProducing(self.transport | |
1762 ) | |
1763 elif part.mime: | |
1764 hdrs = _formatHeaders(msg.getHeaders(True)) | |
1765 _w(str(part) + ' ' + _literal(hdrs)) | |
1766 elif part.empty: | |
1767 _w(str(part) + ' ') | |
1768 _f() | |
1769 if part.part: | |
1770 return FileProducer(msg.getBodyFile() | |
1771 ).beginProducing(self.transport | |
1772 ) | |
1773 else: | |
1774 mf = IMessageFile(msg, None) | |
1775 if mf is not None: | |
1776 return FileProducer(mf.open()).beginProducing(self.transport
) | |
1777 return MessageProducer(msg, None, self._scheduler).beginProducin
g(self.transport) | |
1778 | |
1779 else: | |
1780 _w('BODY ' + collapseNestedLists([getBodyStructure(msg)])) | |
1781 | |
1782 def spewMessage(self, id, msg, query, uid): | |
1783 wbuf = WriteBuffer(self.transport) | |
1784 write = wbuf.write | |
1785 flush = wbuf.flush | |
1786 def start(): | |
1787 write('* %d FETCH (' % (id,)) | |
1788 def finish(): | |
1789 write(')\r\n') | |
1790 def space(): | |
1791 write(' ') | |
1792 | |
1793 def spew(): | |
1794 seenUID = False | |
1795 start() | |
1796 for part in query: | |
1797 if part.type == 'uid': | |
1798 seenUID = True | |
1799 if part.type == 'body': | |
1800 yield self.spew_body(part, id, msg, write, flush) | |
1801 else: | |
1802 f = getattr(self, 'spew_' + part.type) | |
1803 yield f(id, msg, write, flush) | |
1804 if part is not query[-1]: | |
1805 space() | |
1806 if uid and not seenUID: | |
1807 space() | |
1808 yield self.spew_uid(id, msg, write, flush) | |
1809 finish() | |
1810 flush() | |
1811 return self._scheduler(spew()) | |
1812 | |
1813 def __ebFetch(self, failure, tag): | |
1814 self.setTimeout(self._oldTimeout) | |
1815 del self._oldTimeout | |
1816 log.err(failure) | |
1817 self.sendBadResponse(tag, 'FETCH failed: ' + str(failure.value)) | |
1818 | |
1819 def do_STORE(self, tag, messages, mode, flags, uid=0): | |
1820 mode = mode.upper() | |
1821 silent = mode.endswith('SILENT') | |
1822 if mode.startswith('+'): | |
1823 mode = 1 | |
1824 elif mode.startswith('-'): | |
1825 mode = -1 | |
1826 else: | |
1827 mode = 0 | |
1828 | |
1829 maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallba
cks( | |
1830 self.__cbStore, self.__ebStore, (tag, self.mbox, uid, silent), None,
(tag,), None | |
1831 ) | |
1832 | |
1833 select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist) | |
1834 | |
1835 def __cbStore(self, result, tag, mbox, uid, silent): | |
1836 if result and not silent: | |
1837 for (k, v) in result.iteritems(): | |
1838 if uid: | |
1839 uidstr = ' UID %d' % mbox.getUID(k) | |
1840 else: | |
1841 uidstr = '' | |
1842 self.sendUntaggedResponse('%d FETCH (FLAGS (%s)%s)' % | |
1843 (k, ' '.join(v), uidstr)) | |
1844 self.sendPositiveResponse(tag, 'STORE completed') | |
1845 | |
1846 def __ebStore(self, failure, tag): | |
1847 self.sendBadResponse(tag, 'Server error: ' + str(failure.value)) | |
1848 | |
1849 def do_COPY(self, tag, messages, mailbox, uid=0): | |
1850 mailbox = self._parseMbox(mailbox) | |
1851 maybeDeferred(self.account.select, mailbox | |
1852 ).addCallback(self._cbCopySelectedMailbox, tag, messages, mailbox, u
id | |
1853 ).addErrback(self._ebCopySelectedMailbox, tag | |
1854 ) | |
1855 select_COPY = (do_COPY, arg_seqset, arg_astring) | |
1856 | |
1857 def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid): | |
1858 if not mbox: | |
1859 self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox) | |
1860 else: | |
1861 maybeDeferred(self.mbox.fetch, messages, uid | |
1862 ).addCallback(self.__cbCopy, tag, mbox | |
1863 ).addCallback(self.__cbCopied, tag, mbox | |
1864 ).addErrback(self.__ebCopy, tag | |
1865 ) | |
1866 | |
1867 def _ebCopySelectedMailbox(self, failure, tag): | |
1868 self.sendBadResponse(tag, 'Server error: ' + str(failure.value)) | |
1869 | |
1870 def __cbCopy(self, messages, tag, mbox): | |
1871 # XXX - This should handle failures with a rollback or something | |
1872 addedDeferreds = [] | |
1873 addedIDs = [] | |
1874 failures = [] | |
1875 | |
1876 fastCopyMbox = IMessageCopier(mbox, None) | |
1877 for (id, msg) in messages: | |
1878 if fastCopyMbox is not None: | |
1879 d = maybeDeferred(fastCopyMbox.copy, msg) | |
1880 addedDeferreds.append(d) | |
1881 continue | |
1882 | |
1883 # XXX - The following should be an implementation of IMessageCopier.
copy | |
1884 # on an IMailbox->IMessageCopier adapter. | |
1885 | |
1886 flags = msg.getFlags() | |
1887 date = msg.getInternalDate() | |
1888 | |
1889 body = IMessageFile(msg, None) | |
1890 if body is not None: | |
1891 bodyFile = body.open() | |
1892 d = maybeDeferred(mbox.addMessage, bodyFile, flags, date) | |
1893 else: | |
1894 def rewind(f): | |
1895 f.seek(0) | |
1896 return f | |
1897 buffer = tempfile.TemporaryFile() | |
1898 d = MessageProducer(msg, buffer, self._scheduler | |
1899 ).beginProducing(None | |
1900 ).addCallback(lambda _, b=buffer, f=flags, d=date: mbox.addM
essage(rewind(b), f, d) | |
1901 ) | |
1902 addedDeferreds.append(d) | |
1903 return defer.DeferredList(addedDeferreds) | |
1904 | |
1905 def __cbCopied(self, deferredIds, tag, mbox): | |
1906 ids = [] | |
1907 failures = [] | |
1908 for (status, result) in deferredIds: | |
1909 if status: | |
1910 ids.append(result) | |
1911 else: | |
1912 failures.append(result.value) | |
1913 if failures: | |
1914 self.sendNegativeResponse(tag, '[ALERT] Some messages were not copie
d') | |
1915 else: | |
1916 self.sendPositiveResponse(tag, 'COPY completed') | |
1917 | |
1918 def __ebCopy(self, failure, tag): | |
1919 self.sendBadResponse(tag, 'COPY failed:' + str(failure.value)) | |
1920 log.err(failure) | |
1921 | |
1922 def do_UID(self, tag, command, line): | |
1923 command = command.upper() | |
1924 | |
1925 if command not in ('COPY', 'FETCH', 'STORE', 'SEARCH'): | |
1926 raise IllegalClientResponse(command) | |
1927 | |
1928 self.dispatchCommand(tag, command, line, uid=1) | |
1929 | |
1930 select_UID = (do_UID, arg_atom, arg_line) | |
1931 # | |
1932 # IMailboxListener implementation | |
1933 # | |
1934 def modeChanged(self, writeable): | |
1935 if writeable: | |
1936 self.sendUntaggedResponse(message='[READ-WRITE]', async=True) | |
1937 else: | |
1938 self.sendUntaggedResponse(message='[READ-ONLY]', async=True) | |
1939 | |
1940 def flagsChanged(self, newFlags): | |
1941 for (mId, flags) in newFlags.iteritems(): | |
1942 msg = '%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags)) | |
1943 self.sendUntaggedResponse(msg, async=True) | |
1944 | |
1945 def newMessages(self, exists, recent): | |
1946 if exists is not None: | |
1947 self.sendUntaggedResponse('%d EXISTS' % exists, async=True) | |
1948 if recent is not None: | |
1949 self.sendUntaggedResponse('%d RECENT' % recent, async=True) | |
1950 | |
1951 | |
1952 class UnhandledResponse(IMAP4Exception): pass | |
1953 | |
1954 class NegativeResponse(IMAP4Exception): pass | |
1955 | |
1956 class NoSupportedAuthentication(IMAP4Exception): | |
1957 def __init__(self, serverSupports, clientSupports): | |
1958 IMAP4Exception.__init__(self, 'No supported authentication schemes avail
able') | |
1959 self.serverSupports = serverSupports | |
1960 self.clientSupports = clientSupports | |
1961 | |
1962 def __str__(self): | |
1963 return (IMAP4Exception.__str__(self) | |
1964 + ': Server supports %r, client supports %r' | |
1965 % (self.serverSupports, self.clientSupports)) | |
1966 | |
1967 class IllegalServerResponse(IMAP4Exception): pass | |
1968 | |
1969 TIMEOUT_ERROR = error.TimeoutError() | |
1970 | |
1971 class IMAP4Client(basic.LineReceiver, policies.TimeoutMixin): | |
1972 """IMAP4 client protocol implementation | |
1973 | |
1974 @ivar state: A string representing the state the connection is currently | |
1975 in. | |
1976 """ | |
1977 implements(IMailboxListener) | |
1978 | |
1979 tags = None | |
1980 waiting = None | |
1981 queued = None | |
1982 tagID = 1 | |
1983 state = None | |
1984 | |
1985 startedTLS = False | |
1986 | |
1987 # Number of seconds to wait before timing out a connection. | |
1988 # If the number is <= 0 no timeout checking will be performed. | |
1989 timeout = 0 | |
1990 | |
1991 # Capabilities are not allowed to change during the session | |
1992 # So cache the first response and use that for all later | |
1993 # lookups | |
1994 _capCache = None | |
1995 | |
1996 _memoryFileLimit = 1024 * 1024 * 10 | |
1997 | |
1998 # Authentication is pluggable. This maps names to IClientAuthentication | |
1999 # objects. | |
2000 authenticators = None | |
2001 | |
2002 STATUS_CODES = ('OK', 'NO', 'BAD', 'PREAUTH', 'BYE') | |
2003 | |
2004 STATUS_TRANSFORMATIONS = { | |
2005 'MESSAGES': int, 'RECENT': int, 'UNSEEN': int | |
2006 } | |
2007 | |
2008 context = None | |
2009 | |
2010 def __init__(self, contextFactory = None): | |
2011 self.tags = {} | |
2012 self.queued = [] | |
2013 self.authenticators = {} | |
2014 self.context = contextFactory | |
2015 | |
2016 self._tag = None | |
2017 self._parts = None | |
2018 self._lastCmd = None | |
2019 | |
2020 def registerAuthenticator(self, auth): | |
2021 """Register a new form of authentication | |
2022 | |
2023 When invoking the authenticate() method of IMAP4Client, the first | |
2024 matching authentication scheme found will be used. The ordering is | |
2025 that in which the server lists support authentication schemes. | |
2026 | |
2027 @type auth: Implementor of C{IClientAuthentication} | |
2028 @param auth: The object to use to perform the client | |
2029 side of this authentication scheme. | |
2030 """ | |
2031 self.authenticators[auth.getName().upper()] = auth | |
2032 | |
2033 def rawDataReceived(self, data): | |
2034 if self.timeout > 0: | |
2035 self.resetTimeout() | |
2036 | |
2037 self._pendingSize -= len(data) | |
2038 if self._pendingSize > 0: | |
2039 self._pendingBuffer.write(data) | |
2040 else: | |
2041 passon = '' | |
2042 if self._pendingSize < 0: | |
2043 data, passon = data[:self._pendingSize], data[self._pendingSize:
] | |
2044 self._pendingBuffer.write(data) | |
2045 rest = self._pendingBuffer | |
2046 self._pendingBuffer = None | |
2047 self._pendingSize = None | |
2048 rest.seek(0, 0) | |
2049 self._parts.append(rest.read()) | |
2050 self.setLineMode(passon.lstrip('\r\n')) | |
2051 | |
2052 # def sendLine(self, line): | |
2053 # print 'S:', repr(line) | |
2054 # return basic.LineReceiver.sendLine(self, line) | |
2055 | |
2056 def _setupForLiteral(self, rest, octets): | |
2057 self._pendingBuffer = self.messageFile(octets) | |
2058 self._pendingSize = octets | |
2059 if self._parts is None: | |
2060 self._parts = [rest, '\r\n'] | |
2061 else: | |
2062 self._parts.extend([rest, '\r\n']) | |
2063 self.setRawMode() | |
2064 | |
2065 def connectionMade(self): | |
2066 if self.timeout > 0: | |
2067 self.setTimeout(self.timeout) | |
2068 | |
2069 def connectionLost(self, reason): | |
2070 """We are no longer connected""" | |
2071 if self.timeout > 0: | |
2072 self.setTimeout(None) | |
2073 if self.queued is not None: | |
2074 queued = self.queued | |
2075 self.queued = None | |
2076 for cmd in queued: | |
2077 cmd.defer.errback(reason) | |
2078 if self.tags is not None: | |
2079 tags = self.tags | |
2080 self.tags = None | |
2081 for cmd in tags.itervalues(): | |
2082 if cmd is not None and cmd.defer is not None: | |
2083 cmd.defer.errback(reason) | |
2084 | |
2085 | |
2086 def lineReceived(self, line): | |
2087 """ | |
2088 Attempt to parse a single line from the server. | |
2089 | |
2090 @type line: C{str} | |
2091 @param line: The line from the server, without the line delimiter. | |
2092 | |
2093 @raise IllegalServerResponse: If the line or some part of the line | |
2094 does not represent an allowed message from the server at this time. | |
2095 """ | |
2096 # print 'C: ' + repr(line) | |
2097 if self.timeout > 0: | |
2098 self.resetTimeout() | |
2099 | |
2100 lastPart = line.rfind('{') | |
2101 if lastPart != -1: | |
2102 lastPart = line[lastPart + 1:] | |
2103 if lastPart.endswith('}'): | |
2104 # It's a literal a-comin' in | |
2105 try: | |
2106 octets = int(lastPart[:-1]) | |
2107 except ValueError: | |
2108 raise IllegalServerResponse(line) | |
2109 if self._parts is None: | |
2110 self._tag, parts = line.split(None, 1) | |
2111 else: | |
2112 parts = line | |
2113 self._setupForLiteral(parts, octets) | |
2114 return | |
2115 | |
2116 if self._parts is None: | |
2117 # It isn't a literal at all | |
2118 self._regularDispatch(line) | |
2119 else: | |
2120 # If an expression is in progress, no tag is required here | |
2121 # Since we didn't find a literal indicator, this expression | |
2122 # is done. | |
2123 self._parts.append(line) | |
2124 tag, rest = self._tag, ''.join(self._parts) | |
2125 self._tag = self._parts = None | |
2126 self.dispatchCommand(tag, rest) | |
2127 | |
2128 def timeoutConnection(self): | |
2129 if self._lastCmd and self._lastCmd.defer is not None: | |
2130 d, self._lastCmd.defer = self._lastCmd.defer, None | |
2131 d.errback(TIMEOUT_ERROR) | |
2132 | |
2133 if self.queued: | |
2134 for cmd in self.queued: | |
2135 if cmd.defer is not None: | |
2136 d, cmd.defer = cmd.defer, d | |
2137 d.errback(TIMEOUT_ERROR) | |
2138 | |
2139 self.transport.loseConnection() | |
2140 | |
2141 def _regularDispatch(self, line): | |
2142 parts = line.split(None, 1) | |
2143 if len(parts) != 2: | |
2144 parts.append('') | |
2145 tag, rest = parts | |
2146 self.dispatchCommand(tag, rest) | |
2147 | |
2148 def messageFile(self, octets): | |
2149 """Create a file to which an incoming message may be written. | |
2150 | |
2151 @type octets: C{int} | |
2152 @param octets: The number of octets which will be written to the file | |
2153 | |
2154 @rtype: Any object which implements C{write(string)} and | |
2155 C{seek(int, int)} | |
2156 @return: A file-like object | |
2157 """ | |
2158 if octets > self._memoryFileLimit: | |
2159 return tempfile.TemporaryFile() | |
2160 else: | |
2161 return StringIO.StringIO() | |
2162 | |
2163 def makeTag(self): | |
2164 tag = '%0.4X' % self.tagID | |
2165 self.tagID += 1 | |
2166 return tag | |
2167 | |
2168 def dispatchCommand(self, tag, rest): | |
2169 if self.state is None: | |
2170 f = self.response_UNAUTH | |
2171 else: | |
2172 f = getattr(self, 'response_' + self.state.upper(), None) | |
2173 if f: | |
2174 try: | |
2175 f(tag, rest) | |
2176 except: | |
2177 log.err() | |
2178 self.transport.loseConnection() | |
2179 else: | |
2180 log.err("Cannot dispatch: %s, %s, %s" % (self.state, tag, rest)) | |
2181 self.transport.loseConnection() | |
2182 | |
2183 def response_UNAUTH(self, tag, rest): | |
2184 if self.state is None: | |
2185 # Server greeting, this is | |
2186 status, rest = rest.split(None, 1) | |
2187 if status.upper() == 'OK': | |
2188 self.state = 'unauth' | |
2189 elif status.upper() == 'PREAUTH': | |
2190 self.state = 'auth' | |
2191 else: | |
2192 # XXX - This is rude. | |
2193 self.transport.loseConnection() | |
2194 raise IllegalServerResponse(tag + ' ' + rest) | |
2195 | |
2196 b, e = rest.find('['), rest.find(']') | |
2197 if b != -1 and e != -1: | |
2198 self.serverGreeting(self.__cbCapabilities(([rest[b:e]], None))) | |
2199 else: | |
2200 self.serverGreeting(None) | |
2201 else: | |
2202 self._defaultHandler(tag, rest) | |
2203 | |
2204 def response_AUTH(self, tag, rest): | |
2205 self._defaultHandler(tag, rest) | |
2206 | |
2207 def _defaultHandler(self, tag, rest): | |
2208 if tag == '*' or tag == '+': | |
2209 if not self.waiting: | |
2210 self._extraInfo([rest]) | |
2211 else: | |
2212 cmd = self.tags[self.waiting] | |
2213 if tag == '+': | |
2214 cmd.continuation(rest) | |
2215 else: | |
2216 cmd.lines.append(rest) | |
2217 else: | |
2218 try: | |
2219 cmd = self.tags[tag] | |
2220 except KeyError: | |
2221 # XXX - This is rude. | |
2222 self.transport.loseConnection() | |
2223 raise IllegalServerResponse(tag + ' ' + rest) | |
2224 else: | |
2225 status, line = rest.split(None, 1) | |
2226 if status == 'OK': | |
2227 # Give them this last line, too | |
2228 cmd.finish(rest, self._extraInfo) | |
2229 else: | |
2230 cmd.defer.errback(IMAP4Exception(line)) | |
2231 del self.tags[tag] | |
2232 self.waiting = None | |
2233 self._flushQueue() | |
2234 | |
2235 def _flushQueue(self): | |
2236 if self.queued: | |
2237 cmd = self.queued.pop(0) | |
2238 t = self.makeTag() | |
2239 self.tags[t] = cmd | |
2240 self.sendLine(cmd.format(t)) | |
2241 self.waiting = t | |
2242 | |
2243 def _extraInfo(self, lines): | |
2244 # XXX - This is terrible. | |
2245 # XXX - Also, this should collapse temporally proximate calls into singl
e | |
2246 # invocations of IMailboxListener methods, where possible. | |
2247 flags = {} | |
2248 recent = exists = None | |
2249 for L in lines: | |
2250 if L.find('EXISTS') != -1: | |
2251 exists = int(L.split()[0]) | |
2252 elif L.find('RECENT') != -1: | |
2253 recent = int(L.split()[0]) | |
2254 elif L.find('READ-ONLY') != -1: | |
2255 self.modeChanged(0) | |
2256 elif L.find('READ-WRITE') != -1: | |
2257 self.modeChanged(1) | |
2258 elif L.find('FETCH') != -1: | |
2259 for (mId, fetched) in self.__cbFetch(([L], None)).iteritems(): | |
2260 sum = [] | |
2261 for f in fetched.get('FLAGS', []): | |
2262 sum.append(f) | |
2263 flags.setdefault(mId, []).extend(sum) | |
2264 else: | |
2265 log.msg('Unhandled unsolicited response: ' + repr(L)) | |
2266 if flags: | |
2267 self.flagsChanged(flags) | |
2268 if recent is not None or exists is not None: | |
2269 self.newMessages(exists, recent) | |
2270 | |
2271 def sendCommand(self, cmd): | |
2272 cmd.defer = defer.Deferred() | |
2273 if self.waiting: | |
2274 self.queued.append(cmd) | |
2275 return cmd.defer | |
2276 t = self.makeTag() | |
2277 self.tags[t] = cmd | |
2278 self.sendLine(cmd.format(t)) | |
2279 self.waiting = t | |
2280 self._lastCmd = cmd | |
2281 return cmd.defer | |
2282 | |
2283 def getCapabilities(self, useCache=1): | |
2284 """Request the capabilities available on this server. | |
2285 | |
2286 This command is allowed in any state of connection. | |
2287 | |
2288 @type useCache: C{bool} | |
2289 @param useCache: Specify whether to use the capability-cache or to | |
2290 re-retrieve the capabilities from the server. Server capabilities | |
2291 should never change, so for normal use, this flag should never be | |
2292 false. | |
2293 | |
2294 @rtype: C{Deferred} | |
2295 @return: A deferred whose callback will be invoked with a | |
2296 dictionary mapping capability types to lists of supported | |
2297 mechanisms, or to None if a support list is not applicable. | |
2298 """ | |
2299 if useCache and self._capCache is not None: | |
2300 return defer.succeed(self._capCache) | |
2301 cmd = 'CAPABILITY' | |
2302 resp = ('CAPABILITY',) | |
2303 d = self.sendCommand(Command(cmd, wantResponse=resp)) | |
2304 d.addCallback(self.__cbCapabilities) | |
2305 return d | |
2306 | |
2307 def __cbCapabilities(self, (lines, tagline)): | |
2308 caps = {} | |
2309 for rest in lines: | |
2310 rest = rest.split()[1:] | |
2311 for cap in rest: | |
2312 parts = cap.split('=', 1) | |
2313 if len(parts) == 1: | |
2314 category, value = parts[0], None | |
2315 else: | |
2316 category, value = parts | |
2317 caps.setdefault(category, []).append(value) | |
2318 | |
2319 # Preserve a non-ideal API for backwards compatibility. It would | |
2320 # probably be entirely sensible to have an object with a wider API than | |
2321 # dict here so this could be presented less insanely. | |
2322 for category in caps: | |
2323 if caps[category] == [None]: | |
2324 caps[category] = None | |
2325 self._capCache = caps | |
2326 return caps | |
2327 | |
2328 def logout(self): | |
2329 """Inform the server that we are done with the connection. | |
2330 | |
2331 This command is allowed in any state of connection. | |
2332 | |
2333 @rtype: C{Deferred} | |
2334 @return: A deferred whose callback will be invoked with None | |
2335 when the proper server acknowledgement has been received. | |
2336 """ | |
2337 d = self.sendCommand(Command('LOGOUT', wantResponse=('BYE',))) | |
2338 d.addCallback(self.__cbLogout) | |
2339 return d | |
2340 | |
2341 def __cbLogout(self, (lines, tagline)): | |
2342 self.transport.loseConnection() | |
2343 # We don't particularly care what the server said | |
2344 return None | |
2345 | |
2346 | |
2347 def noop(self): | |
2348 """Perform no operation. | |
2349 | |
2350 This command is allowed in any state of connection. | |
2351 | |
2352 @rtype: C{Deferred} | |
2353 @return: A deferred whose callback will be invoked with a list | |
2354 of untagged status updates the server responds with. | |
2355 """ | |
2356 d = self.sendCommand(Command('NOOP')) | |
2357 d.addCallback(self.__cbNoop) | |
2358 return d | |
2359 | |
2360 def __cbNoop(self, (lines, tagline)): | |
2361 # Conceivable, this is elidable. | |
2362 # It is, afterall, a no-op. | |
2363 return lines | |
2364 | |
2365 def startTLS(self, contextFactory=None): | |
2366 """ | |
2367 Initiates a 'STARTTLS' request and negotiates the TLS / SSL | |
2368 Handshake. | |
2369 | |
2370 @param contextFactory: The TLS / SSL Context Factory to | |
2371 leverage. If the contextFactory is None the IMAP4Client will | |
2372 either use the current TLS / SSL Context Factory or attempt to | |
2373 create a new one. | |
2374 | |
2375 @type contextFactory: C{ssl.ClientContextFactory} | |
2376 | |
2377 @return: A Deferred which fires when the transport has been | |
2378 secured according to the given contextFactory, or which fails | |
2379 if the transport cannot be secured. | |
2380 """ | |
2381 assert not self.startedTLS, "Client and Server are currently communicati
ng via TLS" | |
2382 | |
2383 if contextFactory is None: | |
2384 contextFactory = self._getContextFactory() | |
2385 | |
2386 if contextFactory is None: | |
2387 return defer.fail(IMAP4Exception( | |
2388 "IMAP4Client requires a TLS context to " | |
2389 "initiate the STARTTLS handshake")) | |
2390 | |
2391 if 'STARTTLS' not in self._capCache: | |
2392 return defer.fail(IMAP4Exception( | |
2393 "Server does not support secure communication " | |
2394 "via TLS / SSL")) | |
2395 | |
2396 tls = interfaces.ITLSTransport(self.transport, None) | |
2397 if tls is None: | |
2398 return defer.fail(IMAP4Exception( | |
2399 "IMAP4Client transport does not implement " | |
2400 "interfaces.ITLSTransport")) | |
2401 | |
2402 d = self.sendCommand(Command('STARTTLS')) | |
2403 d.addCallback(self._startedTLS, contextFactory) | |
2404 d.addCallback(lambda _: self.getCapabilities()) | |
2405 return d | |
2406 | |
2407 | |
2408 def authenticate(self, secret): | |
2409 """Attempt to enter the authenticated state with the server | |
2410 | |
2411 This command is allowed in the Non-Authenticated state. | |
2412 | |
2413 @rtype: C{Deferred} | |
2414 @return: A deferred whose callback is invoked if the authentication | |
2415 succeeds and whose errback will be invoked otherwise. | |
2416 """ | |
2417 if self._capCache is None: | |
2418 d = self.getCapabilities() | |
2419 else: | |
2420 d = defer.succeed(self._capCache) | |
2421 d.addCallback(self.__cbAuthenticate, secret) | |
2422 return d | |
2423 | |
2424 def __cbAuthenticate(self, caps, secret): | |
2425 auths = caps.get('AUTH', ()) | |
2426 for scheme in auths: | |
2427 if scheme.upper() in self.authenticators: | |
2428 cmd = Command('AUTHENTICATE', scheme, (), | |
2429 self.__cbContinueAuth, scheme, | |
2430 secret) | |
2431 return self.sendCommand(cmd) | |
2432 | |
2433 if self.startedTLS: | |
2434 return defer.fail(NoSupportedAuthentication( | |
2435 auths, self.authenticators.keys())) | |
2436 else: | |
2437 def ebStartTLS(err): | |
2438 err.trap(IMAP4Exception) | |
2439 # We couldn't negotiate TLS for some reason | |
2440 return defer.fail(NoSupportedAuthentication( | |
2441 auths, self.authenticators.keys())) | |
2442 | |
2443 d = self.startTLS() | |
2444 d.addErrback(ebStartTLS) | |
2445 d.addCallback(lambda _: self.getCapabilities()) | |
2446 d.addCallback(self.__cbAuthTLS, secret) | |
2447 return d | |
2448 | |
2449 | |
2450 def __cbContinueAuth(self, rest, scheme, secret): | |
2451 try: | |
2452 chal = base64.decodestring(rest + '\n') | |
2453 except binascii.Error: | |
2454 self.sendLine('*') | |
2455 raise IllegalServerResponse(rest) | |
2456 self.transport.loseConnection() | |
2457 else: | |
2458 auth = self.authenticators[scheme] | |
2459 chal = auth.challengeResponse(secret, chal) | |
2460 self.sendLine(base64.encodestring(chal).strip()) | |
2461 | |
2462 def __cbAuthTLS(self, caps, secret): | |
2463 auths = caps.get('AUTH', ()) | |
2464 for scheme in auths: | |
2465 if scheme.upper() in self.authenticators: | |
2466 cmd = Command('AUTHENTICATE', scheme, (), | |
2467 self.__cbContinueAuth, scheme, | |
2468 secret) | |
2469 return self.sendCommand(cmd) | |
2470 raise NoSupportedAuthentication(auths, self.authenticators.keys()) | |
2471 | |
2472 | |
2473 def login(self, username, password): | |
2474 """Authenticate with the server using a username and password | |
2475 | |
2476 This command is allowed in the Non-Authenticated state. If the | |
2477 server supports the STARTTLS capability and our transport supports | |
2478 TLS, TLS is negotiated before the login command is issued. | |
2479 | |
2480 A more secure way to log in is to use C{startTLS} or | |
2481 C{authenticate} or both. | |
2482 | |
2483 @type username: C{str} | |
2484 @param username: The username to log in with | |
2485 | |
2486 @type password: C{str} | |
2487 @param password: The password to log in with | |
2488 | |
2489 @rtype: C{Deferred} | |
2490 @return: A deferred whose callback is invoked if login is successful | |
2491 and whose errback is invoked otherwise. | |
2492 """ | |
2493 d = maybeDeferred(self.getCapabilities) | |
2494 d.addCallback(self.__cbLoginCaps, username, password) | |
2495 return d | |
2496 | |
2497 def serverGreeting(self, caps): | |
2498 """Called when the server has sent us a greeting. | |
2499 | |
2500 @type caps: C{dict} | |
2501 @param caps: Capabilities the server advertised in its greeting. | |
2502 """ | |
2503 | |
2504 def _getContextFactory(self): | |
2505 if self.context is not None: | |
2506 return self.context | |
2507 try: | |
2508 from twisted.internet import ssl | |
2509 except ImportError: | |
2510 return None | |
2511 else: | |
2512 context = ssl.ClientContextFactory() | |
2513 context.method = ssl.SSL.TLSv1_METHOD | |
2514 return context | |
2515 | |
2516 def __cbLoginCaps(self, capabilities, username, password): | |
2517 # If the server advertises STARTTLS, we might want to try to switch to T
LS | |
2518 tryTLS = 'STARTTLS' in capabilities | |
2519 | |
2520 # If our transport supports switching to TLS, we might want to try to sw
itch to TLS. | |
2521 tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not
None | |
2522 | |
2523 # If our transport is not already using TLS, we might want to try to swi
tch to TLS. | |
2524 nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None | |
2525 | |
2526 if not self.startedTLS and tryTLS and tlsableTransport and nontlsTranspo
rt: | |
2527 d = self.startTLS() | |
2528 | |
2529 d.addCallbacks( | |
2530 self.__cbLoginTLS, | |
2531 self.__ebLoginTLS, | |
2532 callbackArgs=(username, password), | |
2533 ) | |
2534 return d | |
2535 else: | |
2536 if nontlsTransport: | |
2537 log.msg("Server has no TLS support. logging in over cleartext!") | |
2538 args = ' '.join((_quote(username), _quote(password))) | |
2539 return self.sendCommand(Command('LOGIN', args)) | |
2540 | |
2541 def _startedTLS(self, result, context): | |
2542 self.transport.startTLS(context) | |
2543 self._capCache = None | |
2544 self.startedTLS = True | |
2545 return result | |
2546 | |
2547 def __cbLoginTLS(self, result, username, password): | |
2548 args = ' '.join((_quote(username), _quote(password))) | |
2549 return self.sendCommand(Command('LOGIN', args)) | |
2550 | |
2551 def __ebLoginTLS(self, failure): | |
2552 log.err(failure) | |
2553 return failure | |
2554 | |
2555 def namespace(self): | |
2556 """Retrieve information about the namespaces available to this account | |
2557 | |
2558 This command is allowed in the Authenticated and Selected states. | |
2559 | |
2560 @rtype: C{Deferred} | |
2561 @return: A deferred whose callback is invoked with namespace | |
2562 information. An example of this information is:: | |
2563 | |
2564 [[['', '/']], [], []] | |
2565 | |
2566 which indicates a single personal namespace called '' with '/' | |
2567 as its hierarchical delimiter, and no shared or user namespaces. | |
2568 """ | |
2569 cmd = 'NAMESPACE' | |
2570 resp = ('NAMESPACE',) | |
2571 d = self.sendCommand(Command(cmd, wantResponse=resp)) | |
2572 d.addCallback(self.__cbNamespace) | |
2573 return d | |
2574 | |
2575 def __cbNamespace(self, (lines, last)): | |
2576 for line in lines: | |
2577 parts = line.split(None, 1) | |
2578 if len(parts) == 2: | |
2579 if parts[0] == 'NAMESPACE': | |
2580 # XXX UGGG parsing hack :( | |
2581 r = parseNestedParens('(' + parts[1] + ')')[0] | |
2582 return [e or [] for e in r] | |
2583 log.err("No NAMESPACE response to NAMESPACE command") | |
2584 return [[], [], []] | |
2585 | |
2586 def select(self, mailbox): | |
2587 """Select a mailbox | |
2588 | |
2589 This command is allowed in the Authenticated and Selected states. | |
2590 | |
2591 @type mailbox: C{str} | |
2592 @param mailbox: The name of the mailbox to select | |
2593 | |
2594 @rtype: C{Deferred} | |
2595 @return: A deferred whose callback is invoked with mailbox | |
2596 information if the select is successful and whose errback is | |
2597 invoked otherwise. Mailbox information consists of a dictionary | |
2598 with the following keys and values:: | |
2599 | |
2600 FLAGS: A list of strings containing the flags settable on | |
2601 messages in this mailbox. | |
2602 | |
2603 EXISTS: An integer indicating the number of messages in this | |
2604 mailbox. | |
2605 | |
2606 RECENT: An integer indicating the number of \"recent\" | |
2607 messages in this mailbox. | |
2608 | |
2609 UNSEEN: An integer indicating the number of messages not | |
2610 flagged \\Seen in this mailbox. | |
2611 | |
2612 PERMANENTFLAGS: A list of strings containing the flags that | |
2613 can be permanently set on messages in this mailbox. | |
2614 | |
2615 UIDVALIDITY: An integer uniquely identifying this mailbox. | |
2616 """ | |
2617 cmd = 'SELECT' | |
2618 args = _prepareMailboxName(mailbox) | |
2619 resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVAL
IDITY') | |
2620 d = self.sendCommand(Command(cmd, args, wantResponse=resp)) | |
2621 d.addCallback(self.__cbSelect, 1) | |
2622 return d | |
2623 | |
2624 def examine(self, mailbox): | |
2625 """Select a mailbox in read-only mode | |
2626 | |
2627 This command is allowed in the Authenticated and Selected states. | |
2628 | |
2629 @type mailbox: C{str} | |
2630 @param mailbox: The name of the mailbox to examine | |
2631 | |
2632 @rtype: C{Deferred} | |
2633 @return: A deferred whose callback is invoked with mailbox | |
2634 information if the examine is successful and whose errback | |
2635 is invoked otherwise. Mailbox information consists of a dictionary | |
2636 with the following keys and values:: | |
2637 | |
2638 'FLAGS': A list of strings containing the flags settable on | |
2639 messages in this mailbox. | |
2640 | |
2641 'EXISTS': An integer indicating the number of messages in this | |
2642 mailbox. | |
2643 | |
2644 'RECENT': An integer indicating the number of \"recent\" | |
2645 messages in this mailbox. | |
2646 | |
2647 'UNSEEN': An integer indicating the number of messages not | |
2648 flagged \\Seen in this mailbox. | |
2649 | |
2650 'PERMANENTFLAGS': A list of strings containing the flags that | |
2651 can be permanently set on messages in this mailbox. | |
2652 | |
2653 'UIDVALIDITY': An integer uniquely identifying this mailbox. | |
2654 """ | |
2655 cmd = 'EXAMINE' | |
2656 args = _prepareMailboxName(mailbox) | |
2657 resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVAL
IDITY') | |
2658 d = self.sendCommand(Command(cmd, args, wantResponse=resp)) | |
2659 d.addCallback(self.__cbSelect, 0) | |
2660 return d | |
2661 | |
2662 def __cbSelect(self, (lines, tagline), rw): | |
2663 # In the absense of specification, we are free to assume: | |
2664 # READ-WRITE access | |
2665 datum = {'READ-WRITE': rw} | |
2666 lines.append(tagline) | |
2667 for parts in lines: | |
2668 split = parts.split() | |
2669 if len(split) == 2: | |
2670 if split[1].upper().strip() == 'EXISTS': | |
2671 try: | |
2672 datum['EXISTS'] = int(split[0]) | |
2673 except ValueError: | |
2674 raise IllegalServerResponse(parts) | |
2675 elif split[1].upper().strip() == 'RECENT': | |
2676 try: | |
2677 datum['RECENT'] = int(split[0]) | |
2678 except ValueError: | |
2679 raise IllegalServerResponse(parts) | |
2680 else: | |
2681 log.err('Unhandled SELECT response (1): ' + parts) | |
2682 elif split[0].upper().strip() == 'FLAGS': | |
2683 split = parts.split(None, 1) | |
2684 datum['FLAGS'] = tuple(parseNestedParens(split[1])[0]) | |
2685 elif split[0].upper().strip() == 'OK': | |
2686 begin = parts.find('[') | |
2687 end = parts.find(']') | |
2688 if begin == -1 or end == -1: | |
2689 raise IllegalServerResponse(parts) | |
2690 else: | |
2691 content = parts[begin+1:end].split(None, 1) | |
2692 if len(content) >= 1: | |
2693 key = content[0].upper() | |
2694 if key == 'READ-ONLY': | |
2695 datum['READ-WRITE'] = 0 | |
2696 elif key == 'READ-WRITE': | |
2697 datum['READ-WRITE'] = 1 | |
2698 elif key == 'UIDVALIDITY': | |
2699 try: | |
2700 datum['UIDVALIDITY'] = int(content[1]) | |
2701 except ValueError: | |
2702 raise IllegalServerResponse(parts) | |
2703 elif key == 'UNSEEN': | |
2704 try: | |
2705 datum['UNSEEN'] = int(content[1]) | |
2706 except ValueError: | |
2707 raise IllegalServerResponse(parts) | |
2708 elif key == 'UIDNEXT': | |
2709 datum['UIDNEXT'] = int(content[1]) | |
2710 elif key == 'PERMANENTFLAGS': | |
2711 datum['PERMANENTFLAGS'] = tuple(parseNestedParens(co
ntent[1])[0]) | |
2712 else: | |
2713 log.err('Unhandled SELECT response (2): ' + parts) | |
2714 else: | |
2715 log.err('Unhandled SELECT response (3): ' + parts) | |
2716 else: | |
2717 log.err('Unhandled SELECT response (4): ' + parts) | |
2718 return datum | |
2719 | |
2720 def create(self, name): | |
2721 """Create a new mailbox on the server | |
2722 | |
2723 This command is allowed in the Authenticated and Selected states. | |
2724 | |
2725 @type name: C{str} | |
2726 @param name: The name of the mailbox to create. | |
2727 | |
2728 @rtype: C{Deferred} | |
2729 @return: A deferred whose callback is invoked if the mailbox creation | |
2730 is successful and whose errback is invoked otherwise. | |
2731 """ | |
2732 return self.sendCommand(Command('CREATE', _prepareMailboxName(name))) | |
2733 | |
2734 def delete(self, name): | |
2735 """Delete a mailbox | |
2736 | |
2737 This command is allowed in the Authenticated and Selected states. | |
2738 | |
2739 @type name: C{str} | |
2740 @param name: The name of the mailbox to delete. | |
2741 | |
2742 @rtype: C{Deferred} | |
2743 @return: A deferred whose calblack is invoked if the mailbox is | |
2744 deleted successfully and whose errback is invoked otherwise. | |
2745 """ | |
2746 return self.sendCommand(Command('DELETE', _prepareMailboxName(name))) | |
2747 | |
2748 def rename(self, oldname, newname): | |
2749 """Rename a mailbox | |
2750 | |
2751 This command is allowed in the Authenticated and Selected states. | |
2752 | |
2753 @type oldname: C{str} | |
2754 @param oldname: The current name of the mailbox to rename. | |
2755 | |
2756 @type newname: C{str} | |
2757 @param newname: The new name to give the mailbox. | |
2758 | |
2759 @rtype: C{Deferred} | |
2760 @return: A deferred whose callback is invoked if the rename is | |
2761 successful and whose errback is invoked otherwise. | |
2762 """ | |
2763 oldname = _prepareMailboxName(oldname) | |
2764 newname = _prepareMailboxName(newname) | |
2765 return self.sendCommand(Command('RENAME', ' '.join((oldname, newname)))) | |
2766 | |
2767 def subscribe(self, name): | |
2768 """Add a mailbox to the subscription list | |
2769 | |
2770 This command is allowed in the Authenticated and Selected states. | |
2771 | |
2772 @type name: C{str} | |
2773 @param name: The mailbox to mark as 'active' or 'subscribed' | |
2774 | |
2775 @rtype: C{Deferred} | |
2776 @return: A deferred whose callback is invoked if the subscription | |
2777 is successful and whose errback is invoked otherwise. | |
2778 """ | |
2779 return self.sendCommand(Command('SUBSCRIBE', _prepareMailboxName(name))) | |
2780 | |
2781 def unsubscribe(self, name): | |
2782 """Remove a mailbox from the subscription list | |
2783 | |
2784 This command is allowed in the Authenticated and Selected states. | |
2785 | |
2786 @type name: C{str} | |
2787 @param name: The mailbox to unsubscribe | |
2788 | |
2789 @rtype: C{Deferred} | |
2790 @return: A deferred whose callback is invoked if the unsubscription | |
2791 is successful and whose errback is invoked otherwise. | |
2792 """ | |
2793 return self.sendCommand(Command('UNSUBSCRIBE', _prepareMailboxName(name)
)) | |
2794 | |
2795 def list(self, reference, wildcard): | |
2796 """List a subset of the available mailboxes | |
2797 | |
2798 This command is allowed in the Authenticated and Selected states. | |
2799 | |
2800 @type reference: C{str} | |
2801 @param reference: The context in which to interpret C{wildcard} | |
2802 | |
2803 @type wildcard: C{str} | |
2804 @param wildcard: The pattern of mailbox names to match, optionally | |
2805 including either or both of the '*' and '%' wildcards. '*' will | |
2806 match zero or more characters and cross hierarchical boundaries. | |
2807 '%' will also match zero or more characters, but is limited to a | |
2808 single hierarchical level. | |
2809 | |
2810 @rtype: C{Deferred} | |
2811 @return: A deferred whose callback is invoked with a list of C{tuple}s, | |
2812 the first element of which is a C{tuple} of mailbox flags, the second | |
2813 element of which is the hierarchy delimiter for this mailbox, and the | |
2814 third of which is the mailbox name; if the command is unsuccessful, | |
2815 the deferred's errback is invoked instead. | |
2816 """ | |
2817 cmd = 'LIST' | |
2818 args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7')) | |
2819 resp = ('LIST',) | |
2820 d = self.sendCommand(Command(cmd, args, wantResponse=resp)) | |
2821 d.addCallback(self.__cbList, 'LIST') | |
2822 return d | |
2823 | |
2824 def lsub(self, reference, wildcard): | |
2825 """List a subset of the subscribed available mailboxes | |
2826 | |
2827 This command is allowed in the Authenticated and Selected states. | |
2828 | |
2829 The parameters and returned object are the same as for the C{list} | |
2830 method, with one slight difference: Only mailboxes which have been | |
2831 subscribed can be included in the resulting list. | |
2832 """ | |
2833 cmd = 'LSUB' | |
2834 args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7')) | |
2835 resp = ('LSUB',) | |
2836 d = self.sendCommand(Command(cmd, args, wantResponse=resp)) | |
2837 d.addCallback(self.__cbList, 'LSUB') | |
2838 return d | |
2839 | |
2840 def __cbList(self, (lines, last), command): | |
2841 results = [] | |
2842 for L in lines: | |
2843 parts = parseNestedParens(L) | |
2844 if len(parts) != 4: | |
2845 raise IllegalServerResponse, L | |
2846 if parts[0] == command: | |
2847 parts[1] = tuple(parts[1]) | |
2848 results.append(tuple(parts[1:])) | |
2849 return results | |
2850 | |
2851 def status(self, mailbox, *names): | |
2852 """Retrieve the status of the given mailbox | |
2853 | |
2854 This command is allowed in the Authenticated and Selected states. | |
2855 | |
2856 @type mailbox: C{str} | |
2857 @param mailbox: The name of the mailbox to query | |
2858 | |
2859 @type names: C{str} | |
2860 @param names: The status names to query. These may be any number of: | |
2861 MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, and UNSEEN. | |
2862 | |
2863 @rtype: C{Deferred} | |
2864 @return: A deferred whose callback is invoked with the status informatio
n | |
2865 if the command is successful and whose errback is invoked otherwise. | |
2866 """ | |
2867 cmd = 'STATUS' | |
2868 args = "%s (%s)" % (_prepareMailboxName(mailbox), ' '.join(names)) | |
2869 resp = ('STATUS',) | |
2870 d = self.sendCommand(Command(cmd, args, wantResponse=resp)) | |
2871 d.addCallback(self.__cbStatus) | |
2872 return d | |
2873 | |
2874 def __cbStatus(self, (lines, last)): | |
2875 status = {} | |
2876 for line in lines: | |
2877 parts = parseNestedParens(line) | |
2878 if parts[0] == 'STATUS': | |
2879 items = parts[2] | |
2880 items = [items[i:i+2] for i in range(0, len(items), 2)] | |
2881 status.update(dict(items)) | |
2882 for k in status.keys(): | |
2883 t = self.STATUS_TRANSFORMATIONS.get(k) | |
2884 if t: | |
2885 try: | |
2886 status[k] = t(status[k]) | |
2887 except Exception, e: | |
2888 raise IllegalServerResponse('(%s %s): %s' % (k, status[k], s
tr(e))) | |
2889 return status | |
2890 | |
2891 def append(self, mailbox, message, flags = (), date = None): | |
2892 """Add the given message to the given mailbox. | |
2893 | |
2894 This command is allowed in the Authenticated and Selected states. | |
2895 | |
2896 @type mailbox: C{str} | |
2897 @param mailbox: The mailbox to which to add this message. | |
2898 | |
2899 @type message: Any file-like object | |
2900 @param message: The message to add, in RFC822 format. Newlines | |
2901 in this file should be \\r\\n-style. | |
2902 | |
2903 @type flags: Any iterable of C{str} | |
2904 @param flags: The flags to associated with this message. | |
2905 | |
2906 @type date: C{str} | |
2907 @param date: The date to associate with this message. This should | |
2908 be of the format DD-MM-YYYY HH:MM:SS +/-HHMM. For example, in | |
2909 Eastern Standard Time, on July 1st 2004 at half past 1 PM, | |
2910 \"01-07-2004 13:30:00 -0500\". | |
2911 | |
2912 @rtype: C{Deferred} | |
2913 @return: A deferred whose callback is invoked when this command | |
2914 succeeds or whose errback is invoked if it fails. | |
2915 """ | |
2916 message.seek(0, 2) | |
2917 L = message.tell() | |
2918 message.seek(0, 0) | |
2919 fmt = '%s (%s)%s {%d}' | |
2920 if date: | |
2921 date = ' "%s"' % date | |
2922 else: | |
2923 date = '' | |
2924 cmd = fmt % ( | |
2925 _prepareMailboxName(mailbox), ' '.join(flags), | |
2926 date, L | |
2927 ) | |
2928 d = self.sendCommand(Command('APPEND', cmd, (), self.__cbContinueAppend,
message)) | |
2929 return d | |
2930 | |
2931 def __cbContinueAppend(self, lines, message): | |
2932 s = basic.FileSender() | |
2933 return s.beginFileTransfer(message, self.transport, None | |
2934 ).addCallback(self.__cbFinishAppend) | |
2935 | |
2936 def __cbFinishAppend(self, foo): | |
2937 self.sendLine('') | |
2938 | |
2939 def check(self): | |
2940 """Tell the server to perform a checkpoint | |
2941 | |
2942 This command is allowed in the Selected state. | |
2943 | |
2944 @rtype: C{Deferred} | |
2945 @return: A deferred whose callback is invoked when this command | |
2946 succeeds or whose errback is invoked if it fails. | |
2947 """ | |
2948 return self.sendCommand(Command('CHECK')) | |
2949 | |
2950 def close(self): | |
2951 """Return the connection to the Authenticated state. | |
2952 | |
2953 This command is allowed in the Selected state. | |
2954 | |
2955 Issuing this command will also remove all messages flagged \\Deleted | |
2956 from the selected mailbox if it is opened in read-write mode, | |
2957 otherwise it indicates success by no messages are removed. | |
2958 | |
2959 @rtype: C{Deferred} | |
2960 @return: A deferred whose callback is invoked when the command | |
2961 completes successfully or whose errback is invoked if it fails. | |
2962 """ | |
2963 return self.sendCommand(Command('CLOSE')) | |
2964 | |
2965 def expunge(self): | |
2966 """Return the connection to the Authenticate state. | |
2967 | |
2968 This command is allowed in the Selected state. | |
2969 | |
2970 Issuing this command will perform the same actions as issuing the | |
2971 close command, but will also generate an 'expunge' response for | |
2972 every message deleted. | |
2973 | |
2974 @rtype: C{Deferred} | |
2975 @return: A deferred whose callback is invoked with a list of the | |
2976 'expunge' responses when this command is successful or whose errback | |
2977 is invoked otherwise. | |
2978 """ | |
2979 cmd = 'EXPUNGE' | |
2980 resp = ('EXPUNGE',) | |
2981 d = self.sendCommand(Command(cmd, wantResponse=resp)) | |
2982 d.addCallback(self.__cbExpunge) | |
2983 return d | |
2984 | |
2985 def __cbExpunge(self, (lines, last)): | |
2986 ids = [] | |
2987 for line in lines: | |
2988 parts = line.split(None, 1) | |
2989 if len(parts) == 2: | |
2990 if parts[1] == 'EXPUNGE': | |
2991 try: | |
2992 ids.append(int(parts[0])) | |
2993 except ValueError: | |
2994 raise IllegalServerResponse, line | |
2995 return ids | |
2996 | |
2997 def search(self, *queries, **kwarg): | |
2998 """Search messages in the currently selected mailbox | |
2999 | |
3000 This command is allowed in the Selected state. | |
3001 | |
3002 Any non-zero number of queries are accepted by this method, as | |
3003 returned by the C{Query}, C{Or}, and C{Not} functions. | |
3004 | |
3005 One keyword argument is accepted: if uid is passed in with a non-zero | |
3006 value, the server is asked to return message UIDs instead of message | |
3007 sequence numbers. | |
3008 | |
3009 @rtype: C{Deferred} | |
3010 @return: A deferred whose callback will be invoked with a list of all | |
3011 the message sequence numbers return by the search, or whose errback | |
3012 will be invoked if there is an error. | |
3013 """ | |
3014 if kwarg.get('uid'): | |
3015 cmd = 'UID SEARCH' | |
3016 else: | |
3017 cmd = 'SEARCH' | |
3018 args = ' '.join(queries) | |
3019 d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,))) | |
3020 d.addCallback(self.__cbSearch) | |
3021 return d | |
3022 | |
3023 def __cbSearch(self, (lines, end)): | |
3024 ids = [] | |
3025 for line in lines: | |
3026 parts = line.split(None, 1) | |
3027 if len(parts) == 2: | |
3028 if parts[0] == 'SEARCH': | |
3029 try: | |
3030 ids.extend(map(int, parts[1].split())) | |
3031 except ValueError: | |
3032 raise IllegalServerResponse, line | |
3033 return ids | |
3034 | |
3035 def fetchUID(self, messages, uid=0): | |
3036 """Retrieve the unique identifier for one or more messages | |
3037 | |
3038 This command is allowed in the Selected state. | |
3039 | |
3040 @type messages: C{MessageSet} or C{str} | |
3041 @param messages: A message sequence set | |
3042 | |
3043 @type uid: C{bool} | |
3044 @param uid: Indicates whether the message sequence set is of message | |
3045 numbers or of unique message IDs. | |
3046 | |
3047 @rtype: C{Deferred} | |
3048 @return: A deferred whose callback is invoked with a dict mapping | |
3049 message sequence numbers to unique message identifiers, or whose | |
3050 errback is invoked if there is an error. | |
3051 """ | |
3052 d = self._fetch(messages, useUID=uid, uid=1) | |
3053 d.addCallback(self.__cbFetch) | |
3054 return d | |
3055 | |
3056 def fetchFlags(self, messages, uid=0): | |
3057 """Retrieve the flags for one or more messages | |
3058 | |
3059 This command is allowed in the Selected state. | |
3060 | |
3061 @type messages: C{MessageSet} or C{str} | |
3062 @param messages: The messages for which to retrieve flags. | |
3063 | |
3064 @type uid: C{bool} | |
3065 @param uid: Indicates whether the message sequence set is of message | |
3066 numbers or of unique message IDs. | |
3067 | |
3068 @rtype: C{Deferred} | |
3069 @return: A deferred whose callback is invoked with a dict mapping | |
3070 message numbers to lists of flags, or whose errback is invoked if | |
3071 there is an error. | |
3072 """ | |
3073 d = self._fetch(str(messages), useUID=uid, flags=1) | |
3074 d.addCallback(self.__cbFetch) | |
3075 return d | |
3076 | |
3077 def fetchInternalDate(self, messages, uid=0): | |
3078 """Retrieve the internal date associated with one or more messages | |
3079 | |
3080 This command is allowed in the Selected state. | |
3081 | |
3082 @type messages: C{MessageSet} or C{str} | |
3083 @param messages: The messages for which to retrieve the internal date. | |
3084 | |
3085 @type uid: C{bool} | |
3086 @param uid: Indicates whether the message sequence set is of message | |
3087 numbers or of unique message IDs. | |
3088 | |
3089 @rtype: C{Deferred} | |
3090 @return: A deferred whose callback is invoked with a dict mapping | |
3091 message numbers to date strings, or whose errback is invoked | |
3092 if there is an error. Date strings take the format of | |
3093 \"day-month-year time timezone\". | |
3094 """ | |
3095 d = self._fetch(str(messages), useUID=uid, internaldate=1) | |
3096 d.addCallback(self.__cbFetch) | |
3097 return d | |
3098 | |
3099 def fetchEnvelope(self, messages, uid=0): | |
3100 """Retrieve the envelope data for one or more messages | |
3101 | |
3102 This command is allowed in the Selected state. | |
3103 | |
3104 @type messages: C{MessageSet} or C{str} | |
3105 @param messages: The messages for which to retrieve envelope data. | |
3106 | |
3107 @type uid: C{bool} | |
3108 @param uid: Indicates whether the message sequence set is of message | |
3109 numbers or of unique message IDs. | |
3110 | |
3111 @rtype: C{Deferred} | |
3112 @return: A deferred whose callback is invoked with a dict mapping | |
3113 message numbers to envelope data, or whose errback is invoked | |
3114 if there is an error. Envelope data consists of a sequence of the | |
3115 date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, | |
3116 and message-id header fields. The date, subject, in-reply-to, and | |
3117 message-id fields are strings, while the from, sender, reply-to, | |
3118 to, cc, and bcc fields contain address data. Address data consists | |
3119 of a sequence of name, source route, mailbox name, and hostname. | |
3120 Fields which are not present for a particular address may be C{None}. | |
3121 """ | |
3122 d = self._fetch(str(messages), useUID=uid, envelope=1) | |
3123 d.addCallback(self.__cbFetch) | |
3124 return d | |
3125 | |
3126 def fetchBodyStructure(self, messages, uid=0): | |
3127 """Retrieve the structure of the body of one or more messages | |
3128 | |
3129 This command is allowed in the Selected state. | |
3130 | |
3131 @type messages: C{MessageSet} or C{str} | |
3132 @param messages: The messages for which to retrieve body structure | |
3133 data. | |
3134 | |
3135 @type uid: C{bool} | |
3136 @param uid: Indicates whether the message sequence set is of message | |
3137 numbers or of unique message IDs. | |
3138 | |
3139 @rtype: C{Deferred} | |
3140 @return: A deferred whose callback is invoked with a dict mapping | |
3141 message numbers to body structure data, or whose errback is invoked | |
3142 if there is an error. Body structure data describes the MIME-IMB | |
3143 format of a message and consists of a sequence of mime type, mime | |
3144 subtype, parameters, content id, description, encoding, and size. | |
3145 The fields following the size field are variable: if the mime | |
3146 type/subtype is message/rfc822, the contained message's envelope | |
3147 information, body structure data, and number of lines of text; if | |
3148 the mime type is text, the number of lines of text. Extension fields | |
3149 may also be included; if present, they are: the MD5 hash of the body, | |
3150 body disposition, body language. | |
3151 """ | |
3152 d = self._fetch(messages, useUID=uid, bodystructure=1) | |
3153 d.addCallback(self.__cbFetch) | |
3154 return d | |
3155 | |
3156 def fetchSimplifiedBody(self, messages, uid=0): | |
3157 """Retrieve the simplified body structure of one or more messages | |
3158 | |
3159 This command is allowed in the Selected state. | |
3160 | |
3161 @type messages: C{MessageSet} or C{str} | |
3162 @param messages: A message sequence set | |
3163 | |
3164 @type uid: C{bool} | |
3165 @param uid: Indicates whether the message sequence set is of message | |
3166 numbers or of unique message IDs. | |
3167 | |
3168 @rtype: C{Deferred} | |
3169 @return: A deferred whose callback is invoked with a dict mapping | |
3170 message numbers to body data, or whose errback is invoked | |
3171 if there is an error. The simplified body structure is the same | |
3172 as the body structure, except that extension fields will never be | |
3173 present. | |
3174 """ | |
3175 d = self._fetch(messages, useUID=uid, body=1) | |
3176 d.addCallback(self.__cbFetch) | |
3177 return d | |
3178 | |
3179 def fetchMessage(self, messages, uid=0): | |
3180 """Retrieve one or more entire messages | |
3181 | |
3182 This command is allowed in the Selected state. | |
3183 | |
3184 @type messages: C{MessageSet} or C{str} | |
3185 @param messages: A message sequence set | |
3186 | |
3187 @type uid: C{bool} | |
3188 @param uid: Indicates whether the message sequence set is of message | |
3189 numbers or of unique message IDs. | |
3190 | |
3191 @rtype: C{Deferred} | |
3192 @return: A deferred whose callback is invoked with a dict mapping | |
3193 message objects (as returned by self.messageFile(), file objects by | |
3194 default), to additional information, or whose errback is invoked if | |
3195 there is an error. | |
3196 """ | |
3197 d = self._fetch(messages, useUID=uid, rfc822=1) | |
3198 d.addCallback(self.__cbFetch) | |
3199 return d | |
3200 | |
3201 def fetchHeaders(self, messages, uid=0): | |
3202 """Retrieve headers of one or more messages | |
3203 | |
3204 This command is allowed in the Selected state. | |
3205 | |
3206 @type messages: C{MessageSet} or C{str} | |
3207 @param messages: A message sequence set | |
3208 | |
3209 @type uid: C{bool} | |
3210 @param uid: Indicates whether the message sequence set is of message | |
3211 numbers or of unique message IDs. | |
3212 | |
3213 @rtype: C{Deferred} | |
3214 @return: A deferred whose callback is invoked with a dict mapping | |
3215 message numbers to dicts of message headers, or whose errback is | |
3216 invoked if there is an error. | |
3217 """ | |
3218 d = self._fetch(messages, useUID=uid, rfc822header=1) | |
3219 d.addCallback(self.__cbFetch) | |
3220 return d | |
3221 | |
3222 def fetchBody(self, messages, uid=0): | |
3223 """Retrieve body text of one or more messages | |
3224 | |
3225 This command is allowed in the Selected state. | |
3226 | |
3227 @type messages: C{MessageSet} or C{str} | |
3228 @param messages: A message sequence set | |
3229 | |
3230 @type uid: C{bool} | |
3231 @param uid: Indicates whether the message sequence set is of message | |
3232 numbers or of unique message IDs. | |
3233 | |
3234 @rtype: C{Deferred} | |
3235 @return: A deferred whose callback is invoked with a dict mapping | |
3236 message numbers to file-like objects containing body text, or whose | |
3237 errback is invoked if there is an error. | |
3238 """ | |
3239 d = self._fetch(messages, useUID=uid, rfc822text=1) | |
3240 d.addCallback(self.__cbFetch) | |
3241 return d | |
3242 | |
3243 def fetchSize(self, messages, uid=0): | |
3244 """Retrieve the size, in octets, of one or more messages | |
3245 | |
3246 This command is allowed in the Selected state. | |
3247 | |
3248 @type messages: C{MessageSet} or C{str} | |
3249 @param messages: A message sequence set | |
3250 | |
3251 @type uid: C{bool} | |
3252 @param uid: Indicates whether the message sequence set is of message | |
3253 numbers or of unique message IDs. | |
3254 | |
3255 @rtype: C{Deferred} | |
3256 @return: A deferred whose callback is invoked with a dict mapping | |
3257 message numbers to sizes, or whose errback is invoked if there is | |
3258 an error. | |
3259 """ | |
3260 d = self._fetch(messages, useUID=uid, rfc822size=1) | |
3261 d.addCallback(self.__cbFetch) | |
3262 return d | |
3263 | |
3264 def fetchFull(self, messages, uid=0): | |
3265 """Retrieve several different fields of one or more messages | |
3266 | |
3267 This command is allowed in the Selected state. This is equivalent | |
3268 to issuing all of the C{fetchFlags}, C{fetchInternalDate}, | |
3269 C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody} | |
3270 functions. | |
3271 | |
3272 @type messages: C{MessageSet} or C{str} | |
3273 @param messages: A message sequence set | |
3274 | |
3275 @type uid: C{bool} | |
3276 @param uid: Indicates whether the message sequence set is of message | |
3277 numbers or of unique message IDs. | |
3278 | |
3279 @rtype: C{Deferred} | |
3280 @return: A deferred whose callback is invoked with a dict mapping | |
3281 message numbers to dict of the retrieved data values, or whose | |
3282 errback is invoked if there is an error. They dictionary keys | |
3283 are "flags", "date", "size", "envelope", and "body". | |
3284 """ | |
3285 d = self._fetch( | |
3286 messages, useUID=uid, flags=1, internaldate=1, | |
3287 rfc822size=1, envelope=1, body=1 | |
3288 ) | |
3289 d.addCallback(self.__cbFetch) | |
3290 return d | |
3291 | |
3292 def fetchAll(self, messages, uid=0): | |
3293 """Retrieve several different fields of one or more messages | |
3294 | |
3295 This command is allowed in the Selected state. This is equivalent | |
3296 to issuing all of the C{fetchFlags}, C{fetchInternalDate}, | |
3297 C{fetchSize}, and C{fetchEnvelope} functions. | |
3298 | |
3299 @type messages: C{MessageSet} or C{str} | |
3300 @param messages: A message sequence set | |
3301 | |
3302 @type uid: C{bool} | |
3303 @param uid: Indicates whether the message sequence set is of message | |
3304 numbers or of unique message IDs. | |
3305 | |
3306 @rtype: C{Deferred} | |
3307 @return: A deferred whose callback is invoked with a dict mapping | |
3308 message numbers to dict of the retrieved data values, or whose | |
3309 errback is invoked if there is an error. They dictionary keys | |
3310 are "flags", "date", "size", and "envelope". | |
3311 """ | |
3312 d = self._fetch( | |
3313 messages, useUID=uid, flags=1, internaldate=1, | |
3314 rfc822size=1, envelope=1 | |
3315 ) | |
3316 d.addCallback(self.__cbFetch) | |
3317 return d | |
3318 | |
3319 def fetchFast(self, messages, uid=0): | |
3320 """Retrieve several different fields of one or more messages | |
3321 | |
3322 This command is allowed in the Selected state. This is equivalent | |
3323 to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and | |
3324 C{fetchSize} functions. | |
3325 | |
3326 @type messages: C{MessageSet} or C{str} | |
3327 @param messages: A message sequence set | |
3328 | |
3329 @type uid: C{bool} | |
3330 @param uid: Indicates whether the message sequence set is of message | |
3331 numbers or of unique message IDs. | |
3332 | |
3333 @rtype: C{Deferred} | |
3334 @return: A deferred whose callback is invoked with a dict mapping | |
3335 message numbers to dict of the retrieved data values, or whose | |
3336 errback is invoked if there is an error. They dictionary keys are | |
3337 "flags", "date", and "size". | |
3338 """ | |
3339 d = self._fetch( | |
3340 messages, useUID=uid, flags=1, internaldate=1, rfc822size=1 | |
3341 ) | |
3342 d.addCallback(self.__cbFetch) | |
3343 return d | |
3344 | |
3345 def __cbFetch(self, (lines, last)): | |
3346 flags = {} | |
3347 for line in lines: | |
3348 parts = line.split(None, 2) | |
3349 if len(parts) == 3: | |
3350 if parts[1] == 'FETCH': | |
3351 try: | |
3352 id = int(parts[0]) | |
3353 except ValueError: | |
3354 raise IllegalServerResponse, line | |
3355 else: | |
3356 data = parseNestedParens(parts[2]) | |
3357 while len(data) == 1 and isinstance(data, types.ListType
): | |
3358 data = data[0] | |
3359 while data: | |
3360 if len(data) < 2: | |
3361 raise IllegalServerResponse("Not enough argument
s", data) | |
3362 flags.setdefault(id, {})[data[0]] = data[1] | |
3363 del data[:2] | |
3364 else: | |
3365 print '(2)Ignoring ', parts | |
3366 else: | |
3367 print '(3)Ignoring ', parts | |
3368 return flags | |
3369 | |
3370 def fetchSpecific(self, messages, uid=0, headerType=None, | |
3371 headerNumber=None, headerArgs=None, peek=None, | |
3372 offset=None, length=None): | |
3373 """Retrieve a specific section of one or more messages | |
3374 | |
3375 @type messages: C{MessageSet} or C{str} | |
3376 @param messages: A message sequence set | |
3377 | |
3378 @type uid: C{bool} | |
3379 @param uid: Indicates whether the message sequence set is of message | |
3380 numbers or of unique message IDs. | |
3381 | |
3382 @type headerType: C{str} | |
3383 @param headerType: If specified, must be one of HEADER, | |
3384 HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, or TEXT, and will determine | |
3385 which part of the message is retrieved. For HEADER.FIELDS and | |
3386 HEADER.FIELDS.NOT, C{headerArgs} must be a sequence of header names. | |
3387 For MIME, C{headerNumber} must be specified. | |
3388 | |
3389 @type headerNumber: C{int} or C{int} sequence | |
3390 @param headerNumber: The nested rfc822 index specifying the | |
3391 entity to retrieve. For example, C{1} retrieves the first | |
3392 entity of the message, and C{(2, 1, 3}) retrieves the 3rd | |
3393 entity inside the first entity inside the second entity of | |
3394 the message. | |
3395 | |
3396 @type headerArgs: A sequence of C{str} | |
3397 @param headerArgs: If C{headerType} is HEADER.FIELDS, these are the | |
3398 headers to retrieve. If it is HEADER.FIELDS.NOT, these are the | |
3399 headers to exclude from retrieval. | |
3400 | |
3401 @type peek: C{bool} | |
3402 @param peek: If true, cause the server to not set the \\Seen | |
3403 flag on this message as a result of this command. | |
3404 | |
3405 @type offset: C{int} | |
3406 @param offset: The number of octets at the beginning of the result | |
3407 to skip. | |
3408 | |
3409 @type length: C{int} | |
3410 @param length: The number of octets to retrieve. | |
3411 | |
3412 @rtype: C{Deferred} | |
3413 @return: A deferred whose callback is invoked with a mapping of | |
3414 message numbers to retrieved data, or whose errback is invoked | |
3415 if there is an error. | |
3416 """ | |
3417 fmt = '%s BODY%s[%s%s%s]%s' | |
3418 if headerNumber is None: | |
3419 number = '' | |
3420 elif isinstance(headerNumber, types.IntType): | |
3421 number = str(headerNumber) | |
3422 else: | |
3423 number = '.'.join(headerNumber) | |
3424 if headerType is None: | |
3425 header = '' | |
3426 elif number: | |
3427 header = '.' + headerType | |
3428 else: | |
3429 header = headerType | |
3430 if header: | |
3431 if headerArgs is not None: | |
3432 payload = ' (%s)' % ' '.join(headerArgs) | |
3433 else: | |
3434 payload = ' ()' | |
3435 else: | |
3436 payload = '' | |
3437 if offset is None: | |
3438 extra = '' | |
3439 else: | |
3440 extra = '<%d.%d>' % (offset, length) | |
3441 fetch = uid and 'UID FETCH' or 'FETCH' | |
3442 cmd = fmt % (messages, peek and '.PEEK' or '', number, header, payload,
extra) | |
3443 d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',))) | |
3444 d.addCallback(self.__cbFetchSpecific) | |
3445 return d | |
3446 | |
3447 def __cbFetchSpecific(self, (lines, last)): | |
3448 info = {} | |
3449 for line in lines: | |
3450 parts = line.split(None, 2) | |
3451 if len(parts) == 3: | |
3452 if parts[1] == 'FETCH': | |
3453 try: | |
3454 id = int(parts[0]) | |
3455 except ValueError: | |
3456 raise IllegalServerResponse, line | |
3457 else: | |
3458 info[id] = parseNestedParens(parts[2]) | |
3459 return info | |
3460 | |
3461 def _fetch(self, messages, useUID=0, **terms): | |
3462 fetch = useUID and 'UID FETCH' or 'FETCH' | |
3463 | |
3464 if 'rfc822text' in terms: | |
3465 del terms['rfc822text'] | |
3466 terms['rfc822.text'] = True | |
3467 if 'rfc822size' in terms: | |
3468 del terms['rfc822size'] | |
3469 terms['rfc822.size'] = True | |
3470 if 'rfc822header' in terms: | |
3471 del terms['rfc822header'] | |
3472 terms['rfc822.header'] = True | |
3473 | |
3474 cmd = '%s (%s)' % (messages, ' '.join([s.upper() for s in terms.keys()])
) | |
3475 d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',))) | |
3476 return d | |
3477 | |
3478 def setFlags(self, messages, flags, silent=1, uid=0): | |
3479 """Set the flags for one or more messages. | |
3480 | |
3481 This command is allowed in the Selected state. | |
3482 | |
3483 @type messages: C{MessageSet} or C{str} | |
3484 @param messages: A message sequence set | |
3485 | |
3486 @type flags: Any iterable of C{str} | |
3487 @param flags: The flags to set | |
3488 | |
3489 @type silent: C{bool} | |
3490 @param silent: If true, cause the server to supress its verbose | |
3491 response. | |
3492 | |
3493 @type uid: C{bool} | |
3494 @param uid: Indicates whether the message sequence set is of message | |
3495 numbers or of unique message IDs. | |
3496 | |
3497 @rtype: C{Deferred} | |
3498 @return: A deferred whose callback is invoked with a list of the | |
3499 the server's responses (C{[]} if C{silent} is true) or whose | |
3500 errback is invoked if there is an error. | |
3501 """ | |
3502 return self._store(str(messages), silent and 'FLAGS.SILENT' or 'FLAGS',
flags, uid) | |
3503 | |
3504 def addFlags(self, messages, flags, silent=1, uid=0): | |
3505 """Add to the set flags for one or more messages. | |
3506 | |
3507 This command is allowed in the Selected state. | |
3508 | |
3509 @type messages: C{MessageSet} or C{str} | |
3510 @param messages: A message sequence set | |
3511 | |
3512 @type flags: Any iterable of C{str} | |
3513 @param flags: The flags to set | |
3514 | |
3515 @type silent: C{bool} | |
3516 @param silent: If true, cause the server to supress its verbose | |
3517 response. | |
3518 | |
3519 @type uid: C{bool} | |
3520 @param uid: Indicates whether the message sequence set is of message | |
3521 numbers or of unique message IDs. | |
3522 | |
3523 @rtype: C{Deferred} | |
3524 @return: A deferred whose callback is invoked with a list of the | |
3525 the server's responses (C{[]} if C{silent} is true) or whose | |
3526 errback is invoked if there is an error. | |
3527 """ | |
3528 return self._store(str(messages), silent and '+FLAGS.SILENT' or '+FLAGS'
, flags, uid) | |
3529 | |
3530 def removeFlags(self, messages, flags, silent=1, uid=0): | |
3531 """Remove from the set flags for one or more messages. | |
3532 | |
3533 This command is allowed in the Selected state. | |
3534 | |
3535 @type messages: C{MessageSet} or C{str} | |
3536 @param messages: A message sequence set | |
3537 | |
3538 @type flags: Any iterable of C{str} | |
3539 @param flags: The flags to set | |
3540 | |
3541 @type silent: C{bool} | |
3542 @param silent: If true, cause the server to supress its verbose | |
3543 response. | |
3544 | |
3545 @type uid: C{bool} | |
3546 @param uid: Indicates whether the message sequence set is of message | |
3547 numbers or of unique message IDs. | |
3548 | |
3549 @rtype: C{Deferred} | |
3550 @return: A deferred whose callback is invoked with a list of the | |
3551 the server's responses (C{[]} if C{silent} is true) or whose | |
3552 errback is invoked if there is an error. | |
3553 """ | |
3554 return self._store(str(messages), silent and '-FLAGS.SILENT' or '-FLAGS'
, flags, uid) | |
3555 | |
3556 def _store(self, messages, cmd, flags, uid): | |
3557 store = uid and 'UID STORE' or 'STORE' | |
3558 args = ' '.join((messages, cmd, '(%s)' % ' '.join(flags))) | |
3559 d = self.sendCommand(Command(store, args, wantResponse=('FETCH',))) | |
3560 d.addCallback(self.__cbFetch) | |
3561 return d | |
3562 | |
3563 def copy(self, messages, mailbox, uid): | |
3564 """Copy the specified messages to the specified mailbox. | |
3565 | |
3566 This command is allowed in the Selected state. | |
3567 | |
3568 @type messages: C{str} | |
3569 @param messages: A message sequence set | |
3570 | |
3571 @type mailbox: C{str} | |
3572 @param mailbox: The mailbox to which to copy the messages | |
3573 | |
3574 @type uid: C{bool} | |
3575 @param uid: If true, the C{messages} refers to message UIDs, rather | |
3576 than message sequence numbers. | |
3577 | |
3578 @rtype: C{Deferred} | |
3579 @return: A deferred whose callback is invoked with a true value | |
3580 when the copy is successful, or whose errback is invoked if there | |
3581 is an error. | |
3582 """ | |
3583 if uid: | |
3584 cmd = 'UID COPY' | |
3585 else: | |
3586 cmd = 'COPY' | |
3587 args = '%s %s' % (messages, _prepareMailboxName(mailbox)) | |
3588 return self.sendCommand(Command(cmd, args)) | |
3589 | |
3590 # | |
3591 # IMailboxListener methods | |
3592 # | |
3593 def modeChanged(self, writeable): | |
3594 """Override me""" | |
3595 | |
3596 def flagsChanged(self, newFlags): | |
3597 """Override me""" | |
3598 | |
3599 def newMessages(self, exists, recent): | |
3600 """Override me""" | |
3601 | |
3602 | |
3603 class IllegalIdentifierError(IMAP4Exception): pass | |
3604 | |
3605 def parseIdList(s): | |
3606 res = MessageSet() | |
3607 parts = s.split(',') | |
3608 for p in parts: | |
3609 if ':' in p: | |
3610 low, high = p.split(':', 1) | |
3611 try: | |
3612 if low == '*': | |
3613 low = None | |
3614 else: | |
3615 low = long(low) | |
3616 if high == '*': | |
3617 high = None | |
3618 else: | |
3619 high = long(high) | |
3620 res.extend((low, high)) | |
3621 except ValueError: | |
3622 raise IllegalIdentifierError(p) | |
3623 else: | |
3624 try: | |
3625 if p == '*': | |
3626 p = None | |
3627 else: | |
3628 p = long(p) | |
3629 except ValueError: | |
3630 raise IllegalIdentifierError(p) | |
3631 else: | |
3632 res.extend(p) | |
3633 return res | |
3634 | |
3635 class IllegalQueryError(IMAP4Exception): pass | |
3636 | |
3637 _SIMPLE_BOOL = ( | |
3638 'ALL', 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'NEW', 'OLD', 'RECENT', | |
3639 'SEEN', 'UNANSWERED', 'UNDELETED', 'UNDRAFT', 'UNFLAGGED', 'UNSEEN' | |
3640 ) | |
3641 | |
3642 _NO_QUOTES = ( | |
3643 'LARGER', 'SMALLER', 'UID' | |
3644 ) | |
3645 | |
3646 def Query(sorted=0, **kwarg): | |
3647 """Create a query string | |
3648 | |
3649 Among the accepted keywords are:: | |
3650 | |
3651 all : If set to a true value, search all messages in the | |
3652 current mailbox | |
3653 | |
3654 answered : If set to a true value, search messages flagged with | |
3655 \\Answered | |
3656 | |
3657 bcc : A substring to search the BCC header field for | |
3658 | |
3659 before : Search messages with an internal date before this | |
3660 value. The given date should be a string in the format | |
3661 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'. | |
3662 | |
3663 body : A substring to search the body of the messages for | |
3664 | |
3665 cc : A substring to search the CC header field for | |
3666 | |
3667 deleted : If set to a true value, search messages flagged with | |
3668 \\Deleted | |
3669 | |
3670 draft : If set to a true value, search messages flagged with | |
3671 \\Draft | |
3672 | |
3673 flagged : If set to a true value, search messages flagged with | |
3674 \\Flagged | |
3675 | |
3676 from : A substring to search the From header field for | |
3677 | |
3678 header : A two-tuple of a header name and substring to search | |
3679 for in that header | |
3680 | |
3681 keyword : Search for messages with the given keyword set | |
3682 | |
3683 larger : Search for messages larger than this number of octets | |
3684 | |
3685 messages : Search only the given message sequence set. | |
3686 | |
3687 new : If set to a true value, search messages flagged with | |
3688 \\Recent but not \\Seen | |
3689 | |
3690 old : If set to a true value, search messages not flagged with | |
3691 \\Recent | |
3692 | |
3693 on : Search messages with an internal date which is on this | |
3694 date. The given date should be a string in the format | |
3695 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'. | |
3696 | |
3697 recent : If set to a true value, search for messages flagged with | |
3698 \\Recent | |
3699 | |
3700 seen : If set to a true value, search for messages flagged with | |
3701 \\Seen | |
3702 | |
3703 sentbefore : Search for messages with an RFC822 'Date' header before | |
3704 this date. The given date should be a string in the forma
t | |
3705 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'. | |
3706 | |
3707 senton : Search for messages with an RFC822 'Date' header which is | |
3708 on this date The given date should be a string in the for
mat | |
3709 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'. | |
3710 | |
3711 sentsince : Search for messages with an RFC822 'Date' header which is | |
3712 after this date. The given date should be a string in the
format | |
3713 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'. | |
3714 | |
3715 since : Search for messages with an internal date that is after | |
3716 this date.. The given date should be a string in the form
at | |
3717 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'. | |
3718 | |
3719 smaller : Search for messages smaller than this number of octets | |
3720 | |
3721 subject : A substring to search the 'subject' header for | |
3722 | |
3723 text : A substring to search the entire message for | |
3724 | |
3725 to : A substring to search the 'to' header for | |
3726 | |
3727 uid : Search only the messages in the given message set | |
3728 | |
3729 unanswered : If set to a true value, search for messages not | |
3730 flagged with \\Answered | |
3731 | |
3732 undeleted : If set to a true value, search for messages not | |
3733 flagged with \\Deleted | |
3734 | |
3735 undraft : If set to a true value, search for messages not | |
3736 flagged with \\Draft | |
3737 | |
3738 unflagged : If set to a true value, search for messages not | |
3739 flagged with \\Flagged | |
3740 | |
3741 unkeyword : Search for messages without the given keyword set | |
3742 | |
3743 unseen : If set to a true value, search for messages not | |
3744 flagged with \\Seen | |
3745 | |
3746 @type sorted: C{bool} | |
3747 @param sorted: If true, the output will be sorted, alphabetically. | |
3748 The standard does not require it, but it makes testing this function | |
3749 easier. The default is zero, and this should be acceptable for any | |
3750 application. | |
3751 | |
3752 @rtype: C{str} | |
3753 @return: The formatted query string | |
3754 """ | |
3755 cmd = [] | |
3756 keys = kwarg.keys() | |
3757 if sorted: | |
3758 keys.sort() | |
3759 for k in keys: | |
3760 v = kwarg[k] | |
3761 k = k.upper() | |
3762 if k in _SIMPLE_BOOL and v: | |
3763 cmd.append(k) | |
3764 elif k == 'HEADER': | |
3765 cmd.extend([k, v[0], '"%s"' % (v[1],)]) | |
3766 elif k not in _NO_QUOTES: | |
3767 cmd.extend([k, '"%s"' % (v,)]) | |
3768 else: | |
3769 cmd.extend([k, '%s' % (v,)]) | |
3770 if len(cmd) > 1: | |
3771 return '(%s)' % ' '.join(cmd) | |
3772 else: | |
3773 return ' '.join(cmd) | |
3774 | |
3775 def Or(*args): | |
3776 """The disjunction of two or more queries""" | |
3777 if len(args) < 2: | |
3778 raise IllegalQueryError, args | |
3779 elif len(args) == 2: | |
3780 return '(OR %s %s)' % args | |
3781 else: | |
3782 return '(OR %s %s)' % (args[0], Or(*args[1:])) | |
3783 | |
3784 def Not(query): | |
3785 """The negation of a query""" | |
3786 return '(NOT %s)' % (query,) | |
3787 | |
3788 class MismatchedNesting(IMAP4Exception): | |
3789 pass | |
3790 | |
3791 class MismatchedQuoting(IMAP4Exception): | |
3792 pass | |
3793 | |
3794 def wildcardToRegexp(wildcard, delim=None): | |
3795 wildcard = wildcard.replace('*', '(?:.*?)') | |
3796 if delim is None: | |
3797 wildcard = wildcard.replace('%', '(?:.*?)') | |
3798 else: | |
3799 wildcard = wildcard.replace('%', '(?:(?:[^%s])*?)' % re.escape(delim)) | |
3800 return re.compile(wildcard, re.I) | |
3801 | |
3802 def splitQuoted(s): | |
3803 """Split a string into whitespace delimited tokens | |
3804 | |
3805 Tokens that would otherwise be separated but are surrounded by \" | |
3806 remain as a single token. Any token that is not quoted and is | |
3807 equal to \"NIL\" is tokenized as C{None}. | |
3808 | |
3809 @type s: C{str} | |
3810 @param s: The string to be split | |
3811 | |
3812 @rtype: C{list} of C{str} | |
3813 @return: A list of the resulting tokens | |
3814 | |
3815 @raise MismatchedQuoting: Raised if an odd number of quotes are present | |
3816 """ | |
3817 s = s.strip() | |
3818 result = [] | |
3819 inQuote = inWord = start = 0 | |
3820 for (i, c) in zip(range(len(s)), s): | |
3821 if c == '"' and not inQuote: | |
3822 inQuote = 1 | |
3823 start = i + 1 | |
3824 elif c == '"' and inQuote: | |
3825 inQuote = 0 | |
3826 result.append(s[start:i]) | |
3827 start = i + 1 | |
3828 elif not inWord and not inQuote and c not in ('"' + string.whitespace): | |
3829 inWord = 1 | |
3830 start = i | |
3831 elif inWord and not inQuote and c in string.whitespace: | |
3832 if s[start:i] == 'NIL': | |
3833 result.append(None) | |
3834 else: | |
3835 result.append(s[start:i]) | |
3836 start = i | |
3837 inWord = 0 | |
3838 if inQuote: | |
3839 raise MismatchedQuoting(s) | |
3840 if inWord: | |
3841 if s[start:] == 'NIL': | |
3842 result.append(None) | |
3843 else: | |
3844 result.append(s[start:]) | |
3845 return result | |
3846 | |
3847 | |
3848 def splitOn(sequence, predicate, transformers): | |
3849 result = [] | |
3850 mode = predicate(sequence[0]) | |
3851 tmp = [sequence[0]] | |
3852 for e in sequence[1:]: | |
3853 p = predicate(e) | |
3854 if p != mode: | |
3855 result.extend(transformers[mode](tmp)) | |
3856 tmp = [e] | |
3857 mode = p | |
3858 else: | |
3859 tmp.append(e) | |
3860 result.extend(transformers[mode](tmp)) | |
3861 return result | |
3862 | |
3863 def collapseStrings(results): | |
3864 """ | |
3865 Turns a list of length-one strings and lists into a list of longer | |
3866 strings and lists. For example, | |
3867 | |
3868 ['a', 'b', ['c', 'd']] is returned as ['ab', ['cd']] | |
3869 | |
3870 @type results: C{list} of C{str} and C{list} | |
3871 @param results: The list to be collapsed | |
3872 | |
3873 @rtype: C{list} of C{str} and C{list} | |
3874 @return: A new list which is the collapsed form of C{results} | |
3875 """ | |
3876 copy = [] | |
3877 begun = None | |
3878 listsList = [isinstance(s, types.ListType) for s in results] | |
3879 | |
3880 pred = lambda e: isinstance(e, types.TupleType) | |
3881 tran = { | |
3882 0: lambda e: splitQuoted(''.join(e)), | |
3883 1: lambda e: [''.join([i[0] for i in e])] | |
3884 } | |
3885 for (i, c, isList) in zip(range(len(results)), results, listsList): | |
3886 if isList: | |
3887 if begun is not None: | |
3888 copy.extend(splitOn(results[begun:i], pred, tran)) | |
3889 begun = None | |
3890 copy.append(collapseStrings(c)) | |
3891 elif begun is None: | |
3892 begun = i | |
3893 if begun is not None: | |
3894 copy.extend(splitOn(results[begun:], pred, tran)) | |
3895 return copy | |
3896 | |
3897 | |
3898 def parseNestedParens(s, handleLiteral = 1): | |
3899 """Parse an s-exp-like string into a more useful data structure. | |
3900 | |
3901 @type s: C{str} | |
3902 @param s: The s-exp-like string to parse | |
3903 | |
3904 @rtype: C{list} of C{str} and C{list} | |
3905 @return: A list containing the tokens present in the input. | |
3906 | |
3907 @raise MismatchedNesting: Raised if the number or placement | |
3908 of opening or closing parenthesis is invalid. | |
3909 """ | |
3910 s = s.strip() | |
3911 inQuote = 0 | |
3912 contentStack = [[]] | |
3913 try: | |
3914 i = 0 | |
3915 L = len(s) | |
3916 while i < L: | |
3917 c = s[i] | |
3918 if inQuote: | |
3919 if c == '\\': | |
3920 contentStack[-1].append(s[i+1]) | |
3921 i += 2 | |
3922 continue | |
3923 elif c == '"': | |
3924 inQuote = not inQuote | |
3925 contentStack[-1].append(c) | |
3926 i += 1 | |
3927 else: | |
3928 if c == '"': | |
3929 contentStack[-1].append(c) | |
3930 inQuote = not inQuote | |
3931 i += 1 | |
3932 elif handleLiteral and c == '{': | |
3933 end = s.find('}', i) | |
3934 if end == -1: | |
3935 raise ValueError, "Malformed literal" | |
3936 literalSize = int(s[i+1:end]) | |
3937 contentStack[-1].append((s[end+3:end+3+literalSize],)) | |
3938 i = end + 3 + literalSize | |
3939 elif c == '(' or c == '[': | |
3940 contentStack.append([]) | |
3941 i += 1 | |
3942 elif c == ')' or c == ']': | |
3943 contentStack[-2].append(contentStack.pop()) | |
3944 i += 1 | |
3945 else: | |
3946 contentStack[-1].append(c) | |
3947 i += 1 | |
3948 except IndexError: | |
3949 raise MismatchedNesting(s) | |
3950 if len(contentStack) != 1: | |
3951 raise MismatchedNesting(s) | |
3952 return collapseStrings(contentStack[0]) | |
3953 | |
3954 def _quote(s): | |
3955 return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),) | |
3956 | |
3957 def _literal(s): | |
3958 return '{%d}\r\n%s' % (len(s), s) | |
3959 | |
3960 class DontQuoteMe: | |
3961 def __init__(self, value): | |
3962 self.value = value | |
3963 | |
3964 def __str__(self): | |
3965 return str(self.value) | |
3966 | |
3967 _ATOM_SPECIALS = '(){ %*"' | |
3968 def _needsQuote(s): | |
3969 if s == '': | |
3970 return 1 | |
3971 for c in s: | |
3972 if c < '\x20' or c > '\x7f': | |
3973 return 1 | |
3974 if c in _ATOM_SPECIALS: | |
3975 return 1 | |
3976 return 0 | |
3977 | |
3978 def _prepareMailboxName(name): | |
3979 name = name.encode('imap4-utf-7') | |
3980 if _needsQuote(name): | |
3981 return _quote(name) | |
3982 return name | |
3983 | |
3984 def _needsLiteral(s): | |
3985 # Change this to "return 1" to wig out stupid clients | |
3986 return '\n' in s or '\r' in s or len(s) > 1000 | |
3987 | |
3988 def collapseNestedLists(items): | |
3989 """Turn a nested list structure into an s-exp-like string. | |
3990 | |
3991 Strings in C{items} will be sent as literals if they contain CR or LF, | |
3992 otherwise they will be quoted. References to None in C{items} will be | |
3993 translated to the atom NIL. Objects with a 'read' attribute will have | |
3994 it called on them with no arguments and the returned string will be | |
3995 inserted into the output as a literal. Integers will be converted to | |
3996 strings and inserted into the output unquoted. Instances of | |
3997 C{DontQuoteMe} will be converted to strings and inserted into the output | |
3998 unquoted. | |
3999 | |
4000 This function used to be much nicer, and only quote things that really | |
4001 needed to be quoted (and C{DontQuoteMe} did not exist), however, many | |
4002 broken IMAP4 clients were unable to deal with this level of sophistication, | |
4003 forcing the current behavior to be adopted for practical reasons. | |
4004 | |
4005 @type items: Any iterable | |
4006 | |
4007 @rtype: C{str} | |
4008 """ | |
4009 pieces = [] | |
4010 for i in items: | |
4011 if i is None: | |
4012 pieces.extend([' ', 'NIL']) | |
4013 elif isinstance(i, (DontQuoteMe, int, long)): | |
4014 pieces.extend([' ', str(i)]) | |
4015 elif isinstance(i, types.StringTypes): | |
4016 if _needsLiteral(i): | |
4017 pieces.extend([' ', '{', str(len(i)), '}', IMAP4Server.delimiter
, i]) | |
4018 else: | |
4019 pieces.extend([' ', _quote(i)]) | |
4020 elif hasattr(i, 'read'): | |
4021 d = i.read() | |
4022 pieces.extend([' ', '{', str(len(d)), '}', IMAP4Server.delimiter, d]
) | |
4023 else: | |
4024 pieces.extend([' ', '(%s)' % (collapseNestedLists(i),)]) | |
4025 return ''.join(pieces[1:]) | |
4026 | |
4027 | |
4028 class IClientAuthentication(Interface): | |
4029 def getName(): | |
4030 """Return an identifier associated with this authentication scheme. | |
4031 | |
4032 @rtype: C{str} | |
4033 """ | |
4034 | |
4035 def challengeResponse(secret, challenge): | |
4036 """Generate a challenge response string""" | |
4037 | |
4038 class CramMD5ClientAuthenticator: | |
4039 implements(IClientAuthentication) | |
4040 | |
4041 def __init__(self, user): | |
4042 self.user = user | |
4043 | |
4044 def getName(self): | |
4045 return "CRAM-MD5" | |
4046 | |
4047 def challengeResponse(self, secret, chal): | |
4048 response = hmac.HMAC(secret, chal).hexdigest() | |
4049 return '%s %s' % (self.user, response) | |
4050 | |
4051 class LOGINAuthenticator: | |
4052 implements(IClientAuthentication) | |
4053 | |
4054 def __init__(self, user): | |
4055 self.user = user | |
4056 self.challengeResponse = self.challengeUsername | |
4057 | |
4058 def getName(self): | |
4059 return "LOGIN" | |
4060 | |
4061 def challengeUsername(self, secret, chal): | |
4062 # Respond to something like "Username:" | |
4063 self.challengeResponse = self.challengeSecret | |
4064 return self.user | |
4065 | |
4066 def challengeSecret(self, secret, chal): | |
4067 # Respond to something like "Password:" | |
4068 return secret | |
4069 | |
4070 class PLAINAuthenticator: | |
4071 implements(IClientAuthentication) | |
4072 | |
4073 def __init__(self, user): | |
4074 self.user = user | |
4075 | |
4076 def getName(self): | |
4077 return "PLAIN" | |
4078 | |
4079 def challengeResponse(self, secret, chal): | |
4080 return '%s\0%s\0' % (self.user, secret) | |
4081 | |
4082 | |
4083 class MailboxException(IMAP4Exception): pass | |
4084 | |
4085 class MailboxCollision(MailboxException): | |
4086 def __str__(self): | |
4087 return 'Mailbox named %s already exists' % self.args | |
4088 | |
4089 class NoSuchMailbox(MailboxException): | |
4090 def __str__(self): | |
4091 return 'No mailbox named %s exists' % self.args | |
4092 | |
4093 class ReadOnlyMailbox(MailboxException): | |
4094 def __str__(self): | |
4095 return 'Mailbox open in read-only state' | |
4096 | |
4097 | |
4098 class IAccount(Interface): | |
4099 """Interface for Account classes | |
4100 | |
4101 Implementors of this interface should consider implementing | |
4102 C{INamespacePresenter}. | |
4103 """ | |
4104 | |
4105 def addMailbox(name, mbox = None): | |
4106 """Add a new mailbox to this account | |
4107 | |
4108 @type name: C{str} | |
4109 @param name: The name associated with this mailbox. It may not | |
4110 contain multiple hierarchical parts. | |
4111 | |
4112 @type mbox: An object implementing C{IMailbox} | |
4113 @param mbox: The mailbox to associate with this name. If C{None}, | |
4114 a suitable default is created and used. | |
4115 | |
4116 @rtype: C{Deferred} or C{bool} | |
4117 @return: A true value if the creation succeeds, or a deferred whose | |
4118 callback will be invoked when the creation succeeds. | |
4119 | |
4120 @raise MailboxException: Raised if this mailbox cannot be added for | |
4121 some reason. This may also be raised asynchronously, if a C{Deferred} | |
4122 is returned. | |
4123 """ | |
4124 | |
4125 def create(pathspec): | |
4126 """Create a new mailbox from the given hierarchical name. | |
4127 | |
4128 @type pathspec: C{str} | |
4129 @param pathspec: The full hierarchical name of a new mailbox to create. | |
4130 If any of the inferior hierarchical names to this one do not exist, | |
4131 they are created as well. | |
4132 | |
4133 @rtype: C{Deferred} or C{bool} | |
4134 @return: A true value if the creation succeeds, or a deferred whose | |
4135 callback will be invoked when the creation succeeds. | |
4136 | |
4137 @raise MailboxException: Raised if this mailbox cannot be added. | |
4138 This may also be raised asynchronously, if a C{Deferred} is | |
4139 returned. | |
4140 """ | |
4141 | |
4142 def select(name, rw=True): | |
4143 """Acquire a mailbox, given its name. | |
4144 | |
4145 @type name: C{str} | |
4146 @param name: The mailbox to acquire | |
4147 | |
4148 @type rw: C{bool} | |
4149 @param rw: If a true value, request a read-write version of this | |
4150 mailbox. If a false value, request a read-only version. | |
4151 | |
4152 @rtype: Any object implementing C{IMailbox} or C{Deferred} | |
4153 @return: The mailbox object, or a C{Deferred} whose callback will | |
4154 be invoked with the mailbox object. None may be returned if the | |
4155 specified mailbox may not be selected for any reason. | |
4156 """ | |
4157 | |
4158 def delete(name): | |
4159 """Delete the mailbox with the specified name. | |
4160 | |
4161 @type name: C{str} | |
4162 @param name: The mailbox to delete. | |
4163 | |
4164 @rtype: C{Deferred} or C{bool} | |
4165 @return: A true value if the mailbox is successfully deleted, or a | |
4166 C{Deferred} whose callback will be invoked when the deletion | |
4167 completes. | |
4168 | |
4169 @raise MailboxException: Raised if this mailbox cannot be deleted. | |
4170 This may also be raised asynchronously, if a C{Deferred} is returned. | |
4171 """ | |
4172 | |
4173 def rename(oldname, newname): | |
4174 """Rename a mailbox | |
4175 | |
4176 @type oldname: C{str} | |
4177 @param oldname: The current name of the mailbox to rename. | |
4178 | |
4179 @type newname: C{str} | |
4180 @param newname: The new name to associate with the mailbox. | |
4181 | |
4182 @rtype: C{Deferred} or C{bool} | |
4183 @return: A true value if the mailbox is successfully renamed, or a | |
4184 C{Deferred} whose callback will be invoked when the rename operation | |
4185 is completed. | |
4186 | |
4187 @raise MailboxException: Raised if this mailbox cannot be | |
4188 renamed. This may also be raised asynchronously, if a C{Deferred} | |
4189 is returned. | |
4190 """ | |
4191 | |
4192 def isSubscribed(name): | |
4193 """Check the subscription status of a mailbox | |
4194 | |
4195 @type name: C{str} | |
4196 @param name: The name of the mailbox to check | |
4197 | |
4198 @rtype: C{Deferred} or C{bool} | |
4199 @return: A true value if the given mailbox is currently subscribed | |
4200 to, a false value otherwise. A C{Deferred} may also be returned | |
4201 whose callback will be invoked with one of these values. | |
4202 """ | |
4203 | |
4204 def subscribe(name): | |
4205 """Subscribe to a mailbox | |
4206 | |
4207 @type name: C{str} | |
4208 @param name: The name of the mailbox to subscribe to | |
4209 | |
4210 @rtype: C{Deferred} or C{bool} | |
4211 @return: A true value if the mailbox is subscribed to successfully, | |
4212 or a Deferred whose callback will be invoked with this value when | |
4213 the subscription is successful. | |
4214 | |
4215 @raise MailboxException: Raised if this mailbox cannot be | |
4216 subscribed to. This may also be raised asynchronously, if a | |
4217 C{Deferred} is returned. | |
4218 """ | |
4219 | |
4220 def unsubscribe(name): | |
4221 """Unsubscribe from a mailbox | |
4222 | |
4223 @type name: C{str} | |
4224 @param name: The name of the mailbox to unsubscribe from | |
4225 | |
4226 @rtype: C{Deferred} or C{bool} | |
4227 @return: A true value if the mailbox is unsubscribed from successfully, | |
4228 or a Deferred whose callback will be invoked with this value when | |
4229 the unsubscription is successful. | |
4230 | |
4231 @raise MailboxException: Raised if this mailbox cannot be | |
4232 unsubscribed from. This may also be raised asynchronously, if a | |
4233 C{Deferred} is returned. | |
4234 """ | |
4235 | |
4236 def listMailboxes(ref, wildcard): | |
4237 """List all the mailboxes that meet a certain criteria | |
4238 | |
4239 @type ref: C{str} | |
4240 @param ref: The context in which to apply the wildcard | |
4241 | |
4242 @type wildcard: C{str} | |
4243 @param wildcard: An expression against which to match mailbox names. | |
4244 '*' matches any number of characters in a mailbox name, and '%' | |
4245 matches similarly, but will not match across hierarchical boundaries. | |
4246 | |
4247 @rtype: C{list} of C{tuple} | |
4248 @return: A list of C{(mailboxName, mailboxObject)} which meet the | |
4249 given criteria. C{mailboxObject} should implement either | |
4250 C{IMailboxInfo} or C{IMailbox}. A Deferred may also be returned. | |
4251 """ | |
4252 | |
4253 class INamespacePresenter(Interface): | |
4254 def getPersonalNamespaces(): | |
4255 """Report the available personal namespaces. | |
4256 | |
4257 Typically there should be only one personal namespace. A common | |
4258 name for it is \"\", and its hierarchical delimiter is usually | |
4259 \"/\". | |
4260 | |
4261 @rtype: iterable of two-tuples of strings | |
4262 @return: The personal namespaces and their hierarchical delimiters. | |
4263 If no namespaces of this type exist, None should be returned. | |
4264 """ | |
4265 | |
4266 def getSharedNamespaces(): | |
4267 """Report the available shared namespaces. | |
4268 | |
4269 Shared namespaces do not belong to any individual user but are | |
4270 usually to one or more of them. Examples of shared namespaces | |
4271 might be \"#news\" for a usenet gateway. | |
4272 | |
4273 @rtype: iterable of two-tuples of strings | |
4274 @return: The shared namespaces and their hierarchical delimiters. | |
4275 If no namespaces of this type exist, None should be returned. | |
4276 """ | |
4277 | |
4278 def getUserNamespaces(): | |
4279 """Report the available user namespaces. | |
4280 | |
4281 These are namespaces that contain folders belonging to other users | |
4282 access to which this account has been granted. | |
4283 | |
4284 @rtype: iterable of two-tuples of strings | |
4285 @return: The user namespaces and their hierarchical delimiters. | |
4286 If no namespaces of this type exist, None should be returned. | |
4287 """ | |
4288 | |
4289 | |
4290 class MemoryAccount(object): | |
4291 implements(IAccount, INamespacePresenter) | |
4292 | |
4293 mailboxes = None | |
4294 subscriptions = None | |
4295 top_id = 0 | |
4296 | |
4297 def __init__(self, name): | |
4298 self.name = name | |
4299 self.mailboxes = {} | |
4300 self.subscriptions = [] | |
4301 | |
4302 def allocateID(self): | |
4303 id = self.top_id | |
4304 self.top_id += 1 | |
4305 return id | |
4306 | |
4307 ## | |
4308 ## IAccount | |
4309 ## | |
4310 def addMailbox(self, name, mbox = None): | |
4311 name = name.upper() | |
4312 if self.mailboxes.has_key(name): | |
4313 raise MailboxCollision, name | |
4314 if mbox is None: | |
4315 mbox = self._emptyMailbox(name, self.allocateID()) | |
4316 self.mailboxes[name] = mbox | |
4317 return 1 | |
4318 | |
4319 def create(self, pathspec): | |
4320 paths = filter(None, pathspec.split('/')) | |
4321 for accum in range(1, len(paths)): | |
4322 try: | |
4323 self.addMailbox('/'.join(paths[:accum])) | |
4324 except MailboxCollision: | |
4325 pass | |
4326 try: | |
4327 self.addMailbox('/'.join(paths)) | |
4328 except MailboxCollision: | |
4329 if not pathspec.endswith('/'): | |
4330 return False | |
4331 return True | |
4332 | |
4333 def _emptyMailbox(self, name, id): | |
4334 raise NotImplementedError | |
4335 | |
4336 def select(self, name, readwrite=1): | |
4337 return self.mailboxes.get(name.upper()) | |
4338 | |
4339 def delete(self, name): | |
4340 name = name.upper() | |
4341 # See if this mailbox exists at all | |
4342 mbox = self.mailboxes.get(name) | |
4343 if not mbox: | |
4344 raise MailboxException("No such mailbox") | |
4345 # See if this box is flagged \Noselect | |
4346 if r'\Noselect' in mbox.getFlags(): | |
4347 # Check for hierarchically inferior mailboxes with this one | |
4348 # as part of their root. | |
4349 for others in self.mailboxes.keys(): | |
4350 if others != name and others.startswith(name): | |
4351 raise MailboxException, "Hierarchically inferior mailboxes e
xist and \\Noselect is set" | |
4352 mbox.destroy() | |
4353 | |
4354 # iff there are no hierarchically inferior names, we will | |
4355 # delete it from our ken. | |
4356 if self._inferiorNames(name) > 1: | |
4357 del self.mailboxes[name] | |
4358 | |
4359 def rename(self, oldname, newname): | |
4360 oldname = oldname.upper() | |
4361 newname = newname.upper() | |
4362 if not self.mailboxes.has_key(oldname): | |
4363 raise NoSuchMailbox, oldname | |
4364 | |
4365 inferiors = self._inferiorNames(oldname) | |
4366 inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors] | |
4367 | |
4368 for (old, new) in inferiors: | |
4369 if self.mailboxes.has_key(new): | |
4370 raise MailboxCollision, new | |
4371 | |
4372 for (old, new) in inferiors: | |
4373 self.mailboxes[new] = self.mailboxes[old] | |
4374 del self.mailboxes[old] | |
4375 | |
4376 def _inferiorNames(self, name): | |
4377 inferiors = [] | |
4378 for infname in self.mailboxes.keys(): | |
4379 if infname.startswith(name): | |
4380 inferiors.append(infname) | |
4381 return inferiors | |
4382 | |
4383 def isSubscribed(self, name): | |
4384 return name.upper() in self.subscriptions | |
4385 | |
4386 def subscribe(self, name): | |
4387 name = name.upper() | |
4388 if name not in self.subscriptions: | |
4389 self.subscriptions.append(name) | |
4390 | |
4391 def unsubscribe(self, name): | |
4392 name = name.upper() | |
4393 if name not in self.subscriptions: | |
4394 raise MailboxException, "Not currently subscribed to " + name | |
4395 self.subscriptions.remove(name) | |
4396 | |
4397 def listMailboxes(self, ref, wildcard): | |
4398 ref = self._inferiorNames(ref.upper()) | |
4399 wildcard = wildcardToRegexp(wildcard, '/') | |
4400 return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)] | |
4401 | |
4402 ## | |
4403 ## INamespacePresenter | |
4404 ## | |
4405 def getPersonalNamespaces(self): | |
4406 return [["", "/"]] | |
4407 | |
4408 def getSharedNamespaces(self): | |
4409 return None | |
4410 | |
4411 def getOtherNamespaces(self): | |
4412 return None | |
4413 | |
4414 | |
4415 | |
4416 _statusRequestDict = { | |
4417 'MESSAGES': 'getMessageCount', | |
4418 'RECENT': 'getRecentCount', | |
4419 'UIDNEXT': 'getUIDNext', | |
4420 'UIDVALIDITY': 'getUIDValidity', | |
4421 'UNSEEN': 'getUnseenCount' | |
4422 } | |
4423 def statusRequestHelper(mbox, names): | |
4424 r = {} | |
4425 for n in names: | |
4426 r[n] = getattr(mbox, _statusRequestDict[n.upper()])() | |
4427 return r | |
4428 | |
4429 def parseAddr(addr): | |
4430 if addr is None: | |
4431 return [(None, None, None),] | |
4432 addrs = email.Utils.getaddresses([addr]) | |
4433 return [[fn or None, None] + addr.split('@') for fn, addr in addrs] | |
4434 | |
4435 def getEnvelope(msg): | |
4436 headers = msg.getHeaders(True) | |
4437 date = headers.get('date') | |
4438 subject = headers.get('subject') | |
4439 from_ = headers.get('from') | |
4440 sender = headers.get('sender', from_) | |
4441 reply_to = headers.get('reply-to', from_) | |
4442 to = headers.get('to') | |
4443 cc = headers.get('cc') | |
4444 bcc = headers.get('bcc') | |
4445 in_reply_to = headers.get('in-reply-to') | |
4446 mid = headers.get('message-id') | |
4447 return (date, subject, parseAddr(from_), parseAddr(sender), | |
4448 reply_to and parseAddr(reply_to), to and parseAddr(to), | |
4449 cc and parseAddr(cc), bcc and parseAddr(bcc), in_reply_to, mid) | |
4450 | |
4451 def getLineCount(msg): | |
4452 # XXX - Super expensive, CACHE THIS VALUE FOR LATER RE-USE | |
4453 # XXX - This must be the number of lines in the ENCODED version | |
4454 lines = 0 | |
4455 for _ in msg.getBodyFile(): | |
4456 lines += 1 | |
4457 return lines | |
4458 | |
4459 def unquote(s): | |
4460 if s[0] == s[-1] == '"': | |
4461 return s[1:-1] | |
4462 return s | |
4463 | |
4464 def getBodyStructure(msg, extended=False): | |
4465 # XXX - This does not properly handle multipart messages | |
4466 # BODYSTRUCTURE is obscenely complex and criminally under-documented. | |
4467 | |
4468 attrs = {} | |
4469 headers = 'content-type', 'content-id', 'content-description', 'content-tran
sfer-encoding' | |
4470 headers = msg.getHeaders(False, *headers) | |
4471 mm = headers.get('content-type') | |
4472 if mm: | |
4473 mm = ''.join(mm.splitlines()) | |
4474 mimetype = mm.split(';') | |
4475 if mimetype: | |
4476 type = mimetype[0].split('/', 1) | |
4477 if len(type) == 1: | |
4478 major = type[0] | |
4479 minor = None | |
4480 elif len(type) == 2: | |
4481 major, minor = type | |
4482 else: | |
4483 major = minor = None | |
4484 attrs = dict([x.strip().lower().split('=', 1) for x in mimetype[1:]]
) | |
4485 else: | |
4486 major = minor = None | |
4487 else: | |
4488 major = minor = None | |
4489 | |
4490 | |
4491 size = str(msg.getSize()) | |
4492 unquotedAttrs = [(k, unquote(v)) for (k, v) in attrs.iteritems()] | |
4493 result = [ | |
4494 major, minor, # Main and Sub MIME types | |
4495 unquotedAttrs, # content-type parameter list | |
4496 headers.get('content-id'), | |
4497 headers.get('content-description'), | |
4498 headers.get('content-transfer-encoding'), | |
4499 size, # Number of octets total | |
4500 ] | |
4501 | |
4502 if major is not None: | |
4503 if major.lower() == 'text': | |
4504 result.append(str(getLineCount(msg))) | |
4505 elif (major.lower(), minor.lower()) == ('message', 'rfc822'): | |
4506 contained = msg.getSubPart(0) | |
4507 result.append(getEnvelope(contained)) | |
4508 result.append(getBodyStructure(contained, False)) | |
4509 result.append(str(getLineCount(contained))) | |
4510 | |
4511 if not extended or major is None: | |
4512 return result | |
4513 | |
4514 if major.lower() != 'multipart': | |
4515 headers = 'content-md5', 'content-disposition', 'content-language' | |
4516 headers = msg.getHeaders(False, *headers) | |
4517 disp = headers.get('content-disposition') | |
4518 | |
4519 # XXX - I dunno if this is really right | |
4520 if disp: | |
4521 disp = disp.split('; ') | |
4522 if len(disp) == 1: | |
4523 disp = (disp[0].lower(), None) | |
4524 elif len(disp) > 1: | |
4525 disp = (disp[0].lower(), [x.split('=') for x in disp[1:]]) | |
4526 | |
4527 result.append(headers.get('content-md5')) | |
4528 result.append(disp) | |
4529 result.append(headers.get('content-language')) | |
4530 else: | |
4531 result = [result] | |
4532 try: | |
4533 i = 0 | |
4534 while True: | |
4535 submsg = msg.getSubPart(i) | |
4536 result.append(getBodyStructure(submsg)) | |
4537 i += 1 | |
4538 except IndexError: | |
4539 result.append(minor) | |
4540 result.append(attrs.items()) | |
4541 | |
4542 # XXX - I dunno if this is really right | |
4543 headers = msg.getHeaders(False, 'content-disposition', 'content-lang
uage') | |
4544 disp = headers.get('content-disposition') | |
4545 if disp: | |
4546 disp = disp.split('; ') | |
4547 if len(disp) == 1: | |
4548 disp = (disp[0].lower(), None) | |
4549 elif len(disp) > 1: | |
4550 disp = (disp[0].lower(), [x.split('=') for x in disp[1:]]) | |
4551 | |
4552 result.append(disp) | |
4553 result.append(headers.get('content-language')) | |
4554 | |
4555 return result | |
4556 | |
4557 class IMessagePart(Interface): | |
4558 def getHeaders(negate, *names): | |
4559 """Retrieve a group of message headers. | |
4560 | |
4561 @type names: C{tuple} of C{str} | |
4562 @param names: The names of the headers to retrieve or omit. | |
4563 | |
4564 @type negate: C{bool} | |
4565 @param negate: If True, indicates that the headers listed in C{names} | |
4566 should be omitted from the return value, rather than included. | |
4567 | |
4568 @rtype: C{dict} | |
4569 @return: A mapping of header field names to header field values | |
4570 """ | |
4571 | |
4572 def getBodyFile(): | |
4573 """Retrieve a file object containing only the body of this message. | |
4574 """ | |
4575 | |
4576 def getSize(): | |
4577 """Retrieve the total size, in octets, of this message. | |
4578 | |
4579 @rtype: C{int} | |
4580 """ | |
4581 | |
4582 def isMultipart(): | |
4583 """Indicate whether this message has subparts. | |
4584 | |
4585 @rtype: C{bool} | |
4586 """ | |
4587 | |
4588 def getSubPart(part): | |
4589 """Retrieve a MIME sub-message | |
4590 | |
4591 @type part: C{int} | |
4592 @param part: The number of the part to retrieve, indexed from 0. | |
4593 | |
4594 @raise IndexError: Raised if the specified part does not exist. | |
4595 @raise TypeError: Raised if this message is not multipart. | |
4596 | |
4597 @rtype: Any object implementing C{IMessagePart}. | |
4598 @return: The specified sub-part. | |
4599 """ | |
4600 | |
4601 class IMessage(IMessagePart): | |
4602 def getUID(): | |
4603 """Retrieve the unique identifier associated with this message. | |
4604 """ | |
4605 | |
4606 def getFlags(): | |
4607 """Retrieve the flags associated with this message. | |
4608 | |
4609 @rtype: C{iterable} | |
4610 @return: The flags, represented as strings. | |
4611 """ | |
4612 | |
4613 def getInternalDate(): | |
4614 """Retrieve the date internally associated with this message. | |
4615 | |
4616 @rtype: C{str} | |
4617 @return: An RFC822-formatted date string. | |
4618 """ | |
4619 | |
4620 class IMessageFile(Interface): | |
4621 """Optional message interface for representing messages as files. | |
4622 | |
4623 If provided by message objects, this interface will be used instead | |
4624 the more complex MIME-based interface. | |
4625 """ | |
4626 def open(): | |
4627 """Return an file-like object opened for reading. | |
4628 | |
4629 Reading from the returned file will return all the bytes | |
4630 of which this message consists. | |
4631 """ | |
4632 | |
4633 class ISearchableMailbox(Interface): | |
4634 def search(query, uid): | |
4635 """Search for messages that meet the given query criteria. | |
4636 | |
4637 If this interface is not implemented by the mailbox, L{IMailbox.fetch} | |
4638 and various methods of L{IMessage} will be used instead. | |
4639 | |
4640 Implementations which wish to offer better performance than the | |
4641 default implementation should implement this interface. | |
4642 | |
4643 @type query: C{list} | |
4644 @param query: The search criteria | |
4645 | |
4646 @type uid: C{bool} | |
4647 @param uid: If true, the IDs specified in the query are UIDs; | |
4648 otherwise they are message sequence IDs. | |
4649 | |
4650 @rtype: C{list} or C{Deferred} | |
4651 @return: A list of message sequence numbers or message UIDs which | |
4652 match the search criteria or a C{Deferred} whose callback will be | |
4653 invoked with such a list. | |
4654 """ | |
4655 | |
4656 class IMessageCopier(Interface): | |
4657 def copy(messageObject): | |
4658 """Copy the given message object into this mailbox. | |
4659 | |
4660 The message object will be one which was previously returned by | |
4661 L{IMailbox.fetch}. | |
4662 | |
4663 Implementations which wish to offer better performance than the | |
4664 default implementation should implement this interface. | |
4665 | |
4666 If this interface is not implemented by the mailbox, IMailbox.addMessage | |
4667 will be used instead. | |
4668 | |
4669 @rtype: C{Deferred} or C{int} | |
4670 @return: Either the UID of the message or a Deferred which fires | |
4671 with the UID when the copy finishes. | |
4672 """ | |
4673 | |
4674 class IMailboxInfo(Interface): | |
4675 """Interface specifying only the methods required for C{listMailboxes}. | |
4676 | |
4677 Implementations can return objects implementing only these methods for | |
4678 return to C{listMailboxes} if it can allow them to operate more | |
4679 efficiently. | |
4680 """ | |
4681 | |
4682 def getFlags(): | |
4683 """Return the flags defined in this mailbox | |
4684 | |
4685 Flags with the \\ prefix are reserved for use as system flags. | |
4686 | |
4687 @rtype: C{list} of C{str} | |
4688 @return: A list of the flags that can be set on messages in this mailbox
. | |
4689 """ | |
4690 | |
4691 def getHierarchicalDelimiter(): | |
4692 """Get the character which delimits namespaces for in this mailbox. | |
4693 | |
4694 @rtype: C{str} | |
4695 """ | |
4696 | |
4697 class IMailbox(IMailboxInfo): | |
4698 def getUIDValidity(): | |
4699 """Return the unique validity identifier for this mailbox. | |
4700 | |
4701 @rtype: C{int} | |
4702 """ | |
4703 | |
4704 def getUIDNext(): | |
4705 """Return the likely UID for the next message added to this mailbox. | |
4706 | |
4707 @rtype: C{int} | |
4708 """ | |
4709 | |
4710 def getUID(message): | |
4711 """Return the UID of a message in the mailbox | |
4712 | |
4713 @type message: C{int} | |
4714 @param message: The message sequence number | |
4715 | |
4716 @rtype: C{int} | |
4717 @return: The UID of the message. | |
4718 """ | |
4719 | |
4720 def getMessageCount(): | |
4721 """Return the number of messages in this mailbox. | |
4722 | |
4723 @rtype: C{int} | |
4724 """ | |
4725 | |
4726 def getRecentCount(): | |
4727 """Return the number of messages with the 'Recent' flag. | |
4728 | |
4729 @rtype: C{int} | |
4730 """ | |
4731 | |
4732 def getUnseenCount(): | |
4733 """Return the number of messages with the 'Unseen' flag. | |
4734 | |
4735 @rtype: C{int} | |
4736 """ | |
4737 | |
4738 def isWriteable(): | |
4739 """Get the read/write status of the mailbox. | |
4740 | |
4741 @rtype: C{int} | |
4742 @return: A true value if write permission is allowed, a false value othe
rwise. | |
4743 """ | |
4744 | |
4745 def destroy(): | |
4746 """Called before this mailbox is deleted, permanently. | |
4747 | |
4748 If necessary, all resources held by this mailbox should be cleaned | |
4749 up here. This function _must_ set the \\Noselect flag on this | |
4750 mailbox. | |
4751 """ | |
4752 | |
4753 def requestStatus(names): | |
4754 """Return status information about this mailbox. | |
4755 | |
4756 Mailboxes which do not intend to do any special processing to | |
4757 generate the return value, C{statusRequestHelper} can be used | |
4758 to build the dictionary by calling the other interface methods | |
4759 which return the data for each name. | |
4760 | |
4761 @type names: Any iterable | |
4762 @param names: The status names to return information regarding. | |
4763 The possible values for each name are: MESSAGES, RECENT, UIDNEXT, | |
4764 UIDVALIDITY, UNSEEN. | |
4765 | |
4766 @rtype: C{dict} or C{Deferred} | |
4767 @return: A dictionary containing status information about the | |
4768 requested names is returned. If the process of looking this | |
4769 information up would be costly, a deferred whose callback will | |
4770 eventually be passed this dictionary is returned instead. | |
4771 """ | |
4772 | |
4773 def addListener(listener): | |
4774 """Add a mailbox change listener | |
4775 | |
4776 @type listener: Any object which implements C{IMailboxListener} | |
4777 @param listener: An object to add to the set of those which will | |
4778 be notified when the contents of this mailbox change. | |
4779 """ | |
4780 | |
4781 def removeListener(listener): | |
4782 """Remove a mailbox change listener | |
4783 | |
4784 @type listener: Any object previously added to and not removed from | |
4785 this mailbox as a listener. | |
4786 @param listener: The object to remove from the set of listeners. | |
4787 | |
4788 @raise ValueError: Raised when the given object is not a listener for | |
4789 this mailbox. | |
4790 """ | |
4791 | |
4792 def addMessage(message, flags = (), date = None): | |
4793 """Add the given message to this mailbox. | |
4794 | |
4795 @type message: A file-like object | |
4796 @param message: The RFC822 formatted message | |
4797 | |
4798 @type flags: Any iterable of C{str} | |
4799 @param flags: The flags to associate with this message | |
4800 | |
4801 @type date: C{str} | |
4802 @param date: If specified, the date to associate with this | |
4803 message. | |
4804 | |
4805 @rtype: C{Deferred} | |
4806 @return: A deferred whose callback is invoked with the message | |
4807 id if the message is added successfully and whose errback is | |
4808 invoked otherwise. | |
4809 | |
4810 @raise ReadOnlyMailbox: Raised if this Mailbox is not open for | |
4811 read-write. | |
4812 """ | |
4813 | |
4814 def expunge(): | |
4815 """Remove all messages flagged \\Deleted. | |
4816 | |
4817 @rtype: C{list} or C{Deferred} | |
4818 @return: The list of message sequence numbers which were deleted, | |
4819 or a C{Deferred} whose callback will be invoked with such a list. | |
4820 | |
4821 @raise ReadOnlyMailbox: Raised if this Mailbox is not open for | |
4822 read-write. | |
4823 """ | |
4824 | |
4825 def fetch(messages, uid): | |
4826 """Retrieve one or more messages. | |
4827 | |
4828 @type messages: C{MessageSet} | |
4829 @param messages: The identifiers of messages to retrieve information | |
4830 about | |
4831 | |
4832 @type uid: C{bool} | |
4833 @param uid: If true, the IDs specified in the query are UIDs; | |
4834 otherwise they are message sequence IDs. | |
4835 | |
4836 @rtype: Any iterable of two-tuples of message sequence numbers and | |
4837 implementors of C{IMessage}. | |
4838 """ | |
4839 | |
4840 def store(messages, flags, mode, uid): | |
4841 """Set the flags of one or more messages. | |
4842 | |
4843 @type messages: A MessageSet object with the list of messages requested | |
4844 @param messages: The identifiers of the messages to set the flags of. | |
4845 | |
4846 @type flags: sequence of C{str} | |
4847 @param flags: The flags to set, unset, or add. | |
4848 | |
4849 @type mode: -1, 0, or 1 | |
4850 @param mode: If mode is -1, these flags should be removed from the | |
4851 specified messages. If mode is 1, these flags should be added to | |
4852 the specified messages. If mode is 0, all existing flags should be | |
4853 cleared and these flags should be added. | |
4854 | |
4855 @type uid: C{bool} | |
4856 @param uid: If true, the IDs specified in the query are UIDs; | |
4857 otherwise they are message sequence IDs. | |
4858 | |
4859 @rtype: C{dict} or C{Deferred} | |
4860 @return: A C{dict} mapping message sequence numbers to sequences of C{st
r} | |
4861 representing the flags set on the message after this operation has | |
4862 been performed, or a C{Deferred} whose callback will be invoked with | |
4863 such a C{dict}. | |
4864 | |
4865 @raise ReadOnlyMailbox: Raised if this mailbox is not open for | |
4866 read-write. | |
4867 """ | |
4868 | |
4869 class ICloseableMailbox(Interface): | |
4870 """A supplementary interface for mailboxes which require cleanup on close. | |
4871 | |
4872 Implementing this interface is optional. If it is implemented, the protocol | |
4873 code will call the close method defined whenever a mailbox is closed. | |
4874 """ | |
4875 def close(): | |
4876 """Close this mailbox. | |
4877 | |
4878 @return: A C{Deferred} which fires when this mailbox | |
4879 has been closed, or None if the mailbox can be closed | |
4880 immediately. | |
4881 """ | |
4882 | |
4883 def _formatHeaders(headers): | |
4884 hdrs = [': '.join((k.title(), '\r\n'.join(v.splitlines()))) for (k, v) | |
4885 in headers.iteritems()] | |
4886 hdrs = '\r\n'.join(hdrs) + '\r\n' | |
4887 return hdrs | |
4888 | |
4889 def subparts(m): | |
4890 i = 0 | |
4891 try: | |
4892 while True: | |
4893 yield m.getSubPart(i) | |
4894 i += 1 | |
4895 except IndexError: | |
4896 pass | |
4897 | |
4898 def iterateInReactor(i): | |
4899 """Consume an interator at most a single iteration per reactor iteration. | |
4900 | |
4901 If the iterator produces a Deferred, the next iteration will not occur | |
4902 until the Deferred fires, otherwise the next iteration will be taken | |
4903 in the next reactor iteration. | |
4904 | |
4905 @rtype: C{Deferred} | |
4906 @return: A deferred which fires (with None) when the iterator is | |
4907 exhausted or whose errback is called if there is an exception. | |
4908 """ | |
4909 from twisted.internet import reactor | |
4910 d = defer.Deferred() | |
4911 def go(last): | |
4912 try: | |
4913 r = i.next() | |
4914 except StopIteration: | |
4915 d.callback(last) | |
4916 except: | |
4917 d.errback() | |
4918 else: | |
4919 if isinstance(r, defer.Deferred): | |
4920 r.addCallback(go) | |
4921 else: | |
4922 reactor.callLater(0, go, r) | |
4923 go(None) | |
4924 return d | |
4925 | |
4926 class MessageProducer: | |
4927 CHUNK_SIZE = 2 ** 2 ** 2 ** 2 | |
4928 | |
4929 def __init__(self, msg, buffer = None, scheduler = None): | |
4930 """Produce this message. | |
4931 | |
4932 @param msg: The message I am to produce. | |
4933 @type msg: L{IMessage} | |
4934 | |
4935 @param buffer: A buffer to hold the message in. If None, I will | |
4936 use a L{tempfile.TemporaryFile}. | |
4937 @type buffer: file-like | |
4938 """ | |
4939 self.msg = msg | |
4940 if buffer is None: | |
4941 buffer = tempfile.TemporaryFile() | |
4942 self.buffer = buffer | |
4943 if scheduler is None: | |
4944 scheduler = iterateInReactor | |
4945 self.scheduler = scheduler | |
4946 self.write = self.buffer.write | |
4947 | |
4948 def beginProducing(self, consumer): | |
4949 self.consumer = consumer | |
4950 return self.scheduler(self._produce()) | |
4951 | |
4952 def _produce(self): | |
4953 headers = self.msg.getHeaders(True) | |
4954 boundary = None | |
4955 if self.msg.isMultipart(): | |
4956 content = headers.get('content-type') | |
4957 parts = [x.split('=', 1) for x in content.split(';')[1:]] | |
4958 parts = dict([(k.lower().strip(), v) for (k, v) in parts]) | |
4959 boundary = parts.get('boundary') | |
4960 if boundary is None: | |
4961 # Bastards | |
4962 boundary = '----=_%f_boundary_%f' % (time.time(), random.random(
)) | |
4963 headers['content-type'] += '; boundary="%s"' % (boundary,) | |
4964 else: | |
4965 if boundary.startswith('"') and boundary.endswith('"'): | |
4966 boundary = boundary[1:-1] | |
4967 | |
4968 self.write(_formatHeaders(headers)) | |
4969 self.write('\r\n') | |
4970 if self.msg.isMultipart(): | |
4971 for p in subparts(self.msg): | |
4972 self.write('\r\n--%s\r\n' % (boundary,)) | |
4973 yield MessageProducer(p, self.buffer, self.scheduler | |
4974 ).beginProducing(None | |
4975 ) | |
4976 self.write('\r\n--%s--\r\n' % (boundary,)) | |
4977 else: | |
4978 f = self.msg.getBodyFile() | |
4979 while True: | |
4980 b = f.read(self.CHUNK_SIZE) | |
4981 if b: | |
4982 self.buffer.write(b) | |
4983 yield None | |
4984 else: | |
4985 break | |
4986 if self.consumer: | |
4987 self.buffer.seek(0, 0) | |
4988 yield FileProducer(self.buffer | |
4989 ).beginProducing(self.consumer | |
4990 ).addCallback(lambda _: self | |
4991 ) | |
4992 | |
4993 class _FetchParser: | |
4994 class Envelope: | |
4995 # Response should be a list of fields from the message: | |
4996 # date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, | |
4997 # and message-id. | |
4998 # | |
4999 # from, sender, reply-to, to, cc, and bcc are themselves lists of | |
5000 # address information: | |
5001 # personal name, source route, mailbox name, host name | |
5002 # | |
5003 # reply-to and sender must not be None. If not present in a message | |
5004 # they should be defaulted to the value of the from field. | |
5005 type = 'envelope' | |
5006 __str__ = lambda self: 'envelope' | |
5007 | |
5008 class Flags: | |
5009 type = 'flags' | |
5010 __str__ = lambda self: 'flags' | |
5011 | |
5012 class InternalDate: | |
5013 type = 'internaldate' | |
5014 __str__ = lambda self: 'internaldate' | |
5015 | |
5016 class RFC822Header: | |
5017 type = 'rfc822header' | |
5018 __str__ = lambda self: 'rfc822.header' | |
5019 | |
5020 class RFC822Text: | |
5021 type = 'rfc822text' | |
5022 __str__ = lambda self: 'rfc822.text' | |
5023 | |
5024 class RFC822Size: | |
5025 type = 'rfc822size' | |
5026 __str__ = lambda self: 'rfc822.size' | |
5027 | |
5028 class RFC822: | |
5029 type = 'rfc822' | |
5030 __str__ = lambda self: 'rfc822' | |
5031 | |
5032 class UID: | |
5033 type = 'uid' | |
5034 __str__ = lambda self: 'uid' | |
5035 | |
5036 class Body: | |
5037 type = 'body' | |
5038 peek = False | |
5039 header = None | |
5040 mime = None | |
5041 text = None | |
5042 part = () | |
5043 empty = False | |
5044 partialBegin = None | |
5045 partialLength = None | |
5046 def __str__(self): | |
5047 base = 'BODY' | |
5048 part = '' | |
5049 separator = '' | |
5050 if self.part: | |
5051 part = '.'.join([str(x + 1) for x in self.part]) | |
5052 separator = '.' | |
5053 # if self.peek: | |
5054 # base += '.PEEK' | |
5055 if self.header: | |
5056 base += '[%s%s%s]' % (part, separator, self.header,) | |
5057 elif self.text: | |
5058 base += '[%s%sTEXT]' % (part, separator) | |
5059 elif self.mime: | |
5060 base += '[%s%sMIME]' % (part, separator) | |
5061 elif self.empty: | |
5062 base += '[%s]' % (part,) | |
5063 if self.partialBegin is not None: | |
5064 base += '<%d.%d>' % (self.partialBegin, self.partialLength) | |
5065 return base | |
5066 | |
5067 class BodyStructure: | |
5068 type = 'bodystructure' | |
5069 __str__ = lambda self: 'bodystructure' | |
5070 | |
5071 # These three aren't top-level, they don't need type indicators | |
5072 class Header: | |
5073 negate = False | |
5074 fields = None | |
5075 part = None | |
5076 def __str__(self): | |
5077 base = 'HEADER' | |
5078 if self.fields: | |
5079 base += '.FIELDS' | |
5080 if self.negate: | |
5081 base += '.NOT' | |
5082 fields = [] | |
5083 for f in self.fields: | |
5084 f = f.title() | |
5085 if _needsQuote(f): | |
5086 f = _quote(f) | |
5087 fields.append(f) | |
5088 base += ' (%s)' % ' '.join(fields) | |
5089 if self.part: | |
5090 base = '.'.join([str(x + 1) for x in self.part]) + '.' + base | |
5091 return base | |
5092 | |
5093 class Text: | |
5094 pass | |
5095 | |
5096 class MIME: | |
5097 pass | |
5098 | |
5099 parts = None | |
5100 | |
5101 _simple_fetch_att = [ | |
5102 ('envelope', Envelope), | |
5103 ('flags', Flags), | |
5104 ('internaldate', InternalDate), | |
5105 ('rfc822.header', RFC822Header), | |
5106 ('rfc822.text', RFC822Text), | |
5107 ('rfc822.size', RFC822Size), | |
5108 ('rfc822', RFC822), | |
5109 ('uid', UID), | |
5110 ('bodystructure', BodyStructure), | |
5111 ] | |
5112 | |
5113 def __init__(self): | |
5114 self.state = ['initial'] | |
5115 self.result = [] | |
5116 self.remaining = '' | |
5117 | |
5118 def parseString(self, s): | |
5119 s = self.remaining + s | |
5120 try: | |
5121 while s or self.state: | |
5122 # print 'Entering state_' + self.state[-1] + ' with', repr(s) | |
5123 state = self.state.pop() | |
5124 try: | |
5125 used = getattr(self, 'state_' + state)(s) | |
5126 except: | |
5127 self.state.append(state) | |
5128 raise | |
5129 else: | |
5130 # print state, 'consumed', repr(s[:used]) | |
5131 s = s[used:] | |
5132 finally: | |
5133 self.remaining = s | |
5134 | |
5135 def state_initial(self, s): | |
5136 # In the initial state, the literals "ALL", "FULL", and "FAST" | |
5137 # are accepted, as is a ( indicating the beginning of a fetch_att | |
5138 # token, as is the beginning of a fetch_att token. | |
5139 if s == '': | |
5140 return 0 | |
5141 | |
5142 l = s.lower() | |
5143 if l.startswith('all'): | |
5144 self.result.extend(( | |
5145 self.Flags(), self.InternalDate(), | |
5146 self.RFC822Size(), self.Envelope() | |
5147 )) | |
5148 return 3 | |
5149 if l.startswith('full'): | |
5150 self.result.extend(( | |
5151 self.Flags(), self.InternalDate(), | |
5152 self.RFC822Size(), self.Envelope(), | |
5153 self.Body() | |
5154 )) | |
5155 return 4 | |
5156 if l.startswith('fast'): | |
5157 self.result.extend(( | |
5158 self.Flags(), self.InternalDate(), self.RFC822Size(), | |
5159 )) | |
5160 return 4 | |
5161 | |
5162 if l.startswith('('): | |
5163 self.state.extend(('close_paren', 'maybe_fetch_att', 'fetch_att')) | |
5164 return 1 | |
5165 | |
5166 self.state.append('fetch_att') | |
5167 return 0 | |
5168 | |
5169 def state_close_paren(self, s): | |
5170 if s.startswith(')'): | |
5171 return 1 | |
5172 raise Exception("Missing )") | |
5173 | |
5174 def state_whitespace(self, s): | |
5175 # Eat up all the leading whitespace | |
5176 if not s or not s[0].isspace(): | |
5177 raise Exception("Whitespace expected, none found") | |
5178 i = 0 | |
5179 for i in range(len(s)): | |
5180 if not s[i].isspace(): | |
5181 break | |
5182 return i | |
5183 | |
5184 def state_maybe_fetch_att(self, s): | |
5185 if not s.startswith(')'): | |
5186 self.state.extend(('maybe_fetch_att', 'fetch_att', 'whitespace')) | |
5187 return 0 | |
5188 | |
5189 def state_fetch_att(self, s): | |
5190 # Allowed fetch_att tokens are "ENVELOPE", "FLAGS", "INTERNALDATE", | |
5191 # "RFC822", "RFC822.HEADER", "RFC822.SIZE", "RFC822.TEXT", "BODY", | |
5192 # "BODYSTRUCTURE", "UID", | |
5193 # "BODY [".PEEK"] [<section>] ["<" <number> "." <nz_number> ">"] | |
5194 | |
5195 l = s.lower() | |
5196 for (name, cls) in self._simple_fetch_att: | |
5197 if l.startswith(name): | |
5198 self.result.append(cls()) | |
5199 return len(name) | |
5200 | |
5201 b = self.Body() | |
5202 if l.startswith('body.peek'): | |
5203 b.peek = True | |
5204 used = 9 | |
5205 elif l.startswith('body'): | |
5206 used = 4 | |
5207 else: | |
5208 raise Exception("Nothing recognized in fetch_att: %s" % (l,)) | |
5209 | |
5210 self.pending_body = b | |
5211 self.state.extend(('got_body', 'maybe_partial', 'maybe_section')) | |
5212 return used | |
5213 | |
5214 def state_got_body(self, s): | |
5215 self.result.append(self.pending_body) | |
5216 del self.pending_body | |
5217 return 0 | |
5218 | |
5219 def state_maybe_section(self, s): | |
5220 if not s.startswith("["): | |
5221 return 0 | |
5222 | |
5223 self.state.extend(('section', 'part_number')) | |
5224 return 1 | |
5225 | |
5226 _partExpr = re.compile(r'(\d+(?:\.\d+)*)\.?') | |
5227 def state_part_number(self, s): | |
5228 m = self._partExpr.match(s) | |
5229 if m is not None: | |
5230 self.parts = [int(p) - 1 for p in m.groups()[0].split('.')] | |
5231 return m.end() | |
5232 else: | |
5233 self.parts = [] | |
5234 return 0 | |
5235 | |
5236 def state_section(self, s): | |
5237 # Grab "HEADER]" or "HEADER.FIELDS (Header list)]" or | |
5238 # "HEADER.FIELDS.NOT (Header list)]" or "TEXT]" or "MIME]" or | |
5239 # just "]". | |
5240 | |
5241 l = s.lower() | |
5242 used = 0 | |
5243 if l.startswith(']'): | |
5244 self.pending_body.empty = True | |
5245 used += 1 | |
5246 elif l.startswith('header]'): | |
5247 h = self.pending_body.header = self.Header() | |
5248 h.negate = True | |
5249 h.fields = () | |
5250 used += 7 | |
5251 elif l.startswith('text]'): | |
5252 self.pending_body.text = self.Text() | |
5253 used += 5 | |
5254 elif l.startswith('mime]'): | |
5255 self.pending_body.mime = self.MIME() | |
5256 used += 5 | |
5257 else: | |
5258 h = self.Header() | |
5259 if l.startswith('header.fields.not'): | |
5260 h.negate = True | |
5261 used += 17 | |
5262 elif l.startswith('header.fields'): | |
5263 used += 13 | |
5264 else: | |
5265 raise Exception("Unhandled section contents: %r" % (l,)) | |
5266 | |
5267 self.pending_body.header = h | |
5268 self.state.extend(('finish_section', 'header_list', 'whitespace')) | |
5269 self.pending_body.part = tuple(self.parts) | |
5270 self.parts = None | |
5271 return used | |
5272 | |
5273 def state_finish_section(self, s): | |
5274 if not s.startswith(']'): | |
5275 raise Exception("section must end with ]") | |
5276 return 1 | |
5277 | |
5278 def state_header_list(self, s): | |
5279 if not s.startswith('('): | |
5280 raise Exception("Header list must begin with (") | |
5281 end = s.find(')') | |
5282 if end == -1: | |
5283 raise Exception("Header list must end with )") | |
5284 | |
5285 headers = s[1:end].split() | |
5286 self.pending_body.header.fields = map(str.upper, headers) | |
5287 return end + 1 | |
5288 | |
5289 def state_maybe_partial(self, s): | |
5290 # Grab <number.number> or nothing at all | |
5291 if not s.startswith('<'): | |
5292 return 0 | |
5293 end = s.find('>') | |
5294 if end == -1: | |
5295 raise Exception("Found < but not >") | |
5296 | |
5297 partial = s[1:end] | |
5298 parts = partial.split('.', 1) | |
5299 if len(parts) != 2: | |
5300 raise Exception("Partial specification did not include two .-delimit
ed integers") | |
5301 begin, length = map(int, parts) | |
5302 self.pending_body.partialBegin = begin | |
5303 self.pending_body.partialLength = length | |
5304 | |
5305 return end + 1 | |
5306 | |
5307 class FileProducer: | |
5308 CHUNK_SIZE = 2 ** 2 ** 2 ** 2 | |
5309 | |
5310 firstWrite = True | |
5311 | |
5312 def __init__(self, f): | |
5313 self.f = f | |
5314 | |
5315 def beginProducing(self, consumer): | |
5316 self.consumer = consumer | |
5317 self.produce = consumer.write | |
5318 d = self._onDone = defer.Deferred() | |
5319 self.consumer.registerProducer(self, False) | |
5320 return d | |
5321 | |
5322 def resumeProducing(self): | |
5323 b = '' | |
5324 if self.firstWrite: | |
5325 b = '{%d}\r\n' % self._size() | |
5326 self.firstWrite = False | |
5327 if not self.f: | |
5328 return | |
5329 b = b + self.f.read(self.CHUNK_SIZE) | |
5330 if not b: | |
5331 self.consumer.unregisterProducer() | |
5332 self._onDone.callback(self) | |
5333 self._onDone = self.f = self.consumer = None | |
5334 else: | |
5335 self.produce(b) | |
5336 | |
5337 def pauseProducing(self): | |
5338 pass | |
5339 | |
5340 def stopProducing(self): | |
5341 pass | |
5342 | |
5343 def _size(self): | |
5344 b = self.f.tell() | |
5345 self.f.seek(0, 2) | |
5346 e = self.f.tell() | |
5347 self.f.seek(b, 0) | |
5348 return e - b | |
5349 | |
5350 def parseTime(s): | |
5351 # XXX - This may require localization :( | |
5352 months = [ | |
5353 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', | |
5354 'nov', 'dec', 'january', 'february', 'march', 'april', 'may', 'june', | |
5355 'july', 'august', 'september', 'october', 'november', 'december' | |
5356 ] | |
5357 expr = { | |
5358 'day': r"(?P<day>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])", | |
5359 'mon': r"(?P<mon>\w+)", | |
5360 'year': r"(?P<year>\d\d\d\d)" | |
5361 } | |
5362 m = re.match('%(day)s-%(mon)s-%(year)s' % expr, s) | |
5363 if not m: | |
5364 raise ValueError, "Cannot parse time string %r" % (s,) | |
5365 d = m.groupdict() | |
5366 try: | |
5367 d['mon'] = 1 + (months.index(d['mon'].lower()) % 12) | |
5368 d['year'] = int(d['year']) | |
5369 d['day'] = int(d['day']) | |
5370 except ValueError: | |
5371 raise ValueError, "Cannot parse time string %r" % (s,) | |
5372 else: | |
5373 return time.struct_time( | |
5374 (d['year'], d['mon'], d['day'], 0, 0, 0, -1, -1, -1) | |
5375 ) | |
5376 | |
5377 import codecs | |
5378 def modified_base64(s): | |
5379 s_utf7 = s.encode('utf-7') | |
5380 return s_utf7[1:-1].replace('/', ',') | |
5381 | |
5382 def modified_unbase64(s): | |
5383 s_utf7 = '+' + s.replace(',', '/') + '-' | |
5384 return s_utf7.decode('utf-7') | |
5385 | |
5386 def encoder(s, errors=None): | |
5387 """ | |
5388 Encode the given C{unicode} string using the IMAP4 specific variation of | |
5389 UTF-7. | |
5390 | |
5391 @type s: C{unicode} | |
5392 @param s: The text to encode. | |
5393 | |
5394 @param errors: Policy for handling encoding errors. Currently ignored. | |
5395 | |
5396 @return: C{tuple} of a C{str} giving the encoded bytes and an C{int} | |
5397 giving the number of code units consumed from the input. | |
5398 """ | |
5399 r = [] | |
5400 _in = [] | |
5401 for c in s: | |
5402 if ord(c) in (range(0x20, 0x26) + range(0x27, 0x7f)): | |
5403 if _in: | |
5404 r.extend(['&', modified_base64(''.join(_in)), '-']) | |
5405 del _in[:] | |
5406 r.append(str(c)) | |
5407 elif c == '&': | |
5408 if _in: | |
5409 r.extend(['&', modified_base64(''.join(_in)), '-']) | |
5410 del _in[:] | |
5411 r.append('&-') | |
5412 else: | |
5413 _in.append(c) | |
5414 if _in: | |
5415 r.extend(['&', modified_base64(''.join(_in)), '-']) | |
5416 return (''.join(r), len(s)) | |
5417 | |
5418 def decoder(s, errors=None): | |
5419 """ | |
5420 Decode the given C{str} using the IMAP4 specific variation of UTF-7. | |
5421 | |
5422 @type s: C{str} | |
5423 @param s: The bytes to decode. | |
5424 | |
5425 @param errors: Policy for handling decoding errors. Currently ignored. | |
5426 | |
5427 @return: a C{tuple} of a C{unicode} string giving the text which was | |
5428 decoded and an C{int} giving the number of bytes consumed from the | |
5429 input. | |
5430 """ | |
5431 r = [] | |
5432 decode = [] | |
5433 for c in s: | |
5434 if c == '&' and not decode: | |
5435 decode.append('&') | |
5436 elif c == '-' and decode: | |
5437 if len(decode) == 1: | |
5438 r.append('&') | |
5439 else: | |
5440 r.append(modified_unbase64(''.join(decode[1:]))) | |
5441 decode = [] | |
5442 elif decode: | |
5443 decode.append(c) | |
5444 else: | |
5445 r.append(c) | |
5446 if decode: | |
5447 r.append(modified_unbase64(''.join(decode[1:]))) | |
5448 return (''.join(r), len(s)) | |
5449 | |
5450 class StreamReader(codecs.StreamReader): | |
5451 def decode(self, s, errors='strict'): | |
5452 return decoder(s) | |
5453 | |
5454 class StreamWriter(codecs.StreamWriter): | |
5455 def decode(self, s, errors='strict'): | |
5456 return encoder(s) | |
5457 | |
5458 def imap4_utf_7(name): | |
5459 if name == 'imap4-utf-7': | |
5460 return (encoder, decoder, StreamReader, StreamWriter) | |
5461 codecs.register(imap4_utf_7) | |
5462 | |
5463 __all__ = [ | |
5464 # Protocol classes | |
5465 'IMAP4Server', 'IMAP4Client', | |
5466 | |
5467 # Interfaces | |
5468 'IMailboxListener', 'IClientAuthentication', 'IAccount', 'IMailbox', | |
5469 'INamespacePresenter', 'ICloseableMailbox', 'IMailboxInfo', | |
5470 'IMessage', 'IMessageCopier', 'IMessageFile', 'ISearchableMailbox', | |
5471 | |
5472 # Exceptions | |
5473 'IMAP4Exception', 'IllegalClientResponse', 'IllegalOperation', | |
5474 'IllegalMailboxEncoding', 'UnhandledResponse', 'NegativeResponse', | |
5475 'NoSupportedAuthentication', 'IllegalServerResponse', | |
5476 'IllegalIdentifierError', 'IllegalQueryError', 'MismatchedNesting', | |
5477 'MismatchedQuoting', 'MailboxException', 'MailboxCollision', | |
5478 'NoSuchMailbox', 'ReadOnlyMailbox', | |
5479 | |
5480 # Auth objects | |
5481 'CramMD5ClientAuthenticator', 'PLAINAuthenticator', 'LOGINAuthenticator', | |
5482 'PLAINCredentials', 'LOGINCredentials', | |
5483 | |
5484 # Simple query interface | |
5485 'Query', 'Not', 'Or', | |
5486 | |
5487 # Miscellaneous | |
5488 'MemoryAccount', | |
5489 'statusRequestHelper', | |
5490 ] | |
OLD | NEW |