Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(137)

Side by Side Diff: third_party/twisted_8_1/twisted/mail/imap4.py

Issue 12261012: Remove third_party/twisted_8_1 (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Created 7 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
OLDNEW
(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 ]
OLDNEW
« no previous file with comments | « third_party/twisted_8_1/twisted/mail/bounce.py ('k') | third_party/twisted_8_1/twisted/mail/mail.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698