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