1 # -*- test-case-name: twisted.mail.test.test_imap -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
6 An IMAP4 protocol implementation
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
33 import cStringIO as StringIO
37 from zope.interface import implements, Interface
39 from twisted.protocols import basic
40 from twisted.protocols import policies
41 from twisted.internet import defer
42 from twisted.internet import error
43 from twisted.internet.defer import maybeDeferred
44 from twisted.python import log, text
45 from twisted.internet import interfaces
47 from twisted import cred
48 import twisted.cred.error
49 import twisted.cred.credentials
52 # locale-independent month names to use instead of strftime's
53 _MONTH_NAMES = dict(zip(
55 "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split()))
58 class MessageSet(object):
60 Essentially an infinite bitfield, with some extra features.
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).
74 def __init__(self, start=_empty, end=_empty):
76 Create a new MessageSet()
78 @type start: Optional C{int}
79 @param start: Start of range, or only message number
81 @type end: Optional C{int}
82 @param end: End of range.
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.
89 if start is self._empty:
92 if isinstance(start, types.ListType):
93 self.ranges = start[:]
100 def _setLast(self, value):
101 if self._last is not self._empty:
102 raise ValueError("last already set")
105 for i, (l, h) in enumerate(self.ranges):
107 break # There are no more Nones after this
113 self.ranges[i] = (l, h)
121 "Highest" message number, refered to by "*".
122 Must be set before attempting to use the MessageSet.
124 return _getLast, _setLast, None, doc
125 last = property(*last())
127 def add(self, start, end=_empty):
132 @param start: Start of range, or only message number
134 @type end: Optional C{int}
135 @param end: End of range.
137 if end is self._empty:
140 if self._last is not self._empty:
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
152 self.ranges.append((start, end))
155 def __add__(self, other):
156 if isinstance(other, MessageSet):
157 ranges = self.ranges + other.ranges
158 return MessageSet(ranges)
160 res = MessageSet(self.ranges)
168 def extend(self, other):
169 if isinstance(other, MessageSet):
170 self.ranges.extend(other.ranges)
183 Clean ranges list, combining adjacent ranges
188 oldl, oldh = None, None
189 for i,(l, h) in enumerate(self.ranges):
192 # l is >= oldl and h is >= oldh due to sort()
193 if oldl is not None and l <= oldh + 1:
196 self.ranges[i - 1] = None
197 self.ranges[i] = (l, h)
201 self.ranges = filter(None, self.ranges)
204 def __contains__(self, value):
206 May raise TypeError if we encounter an open-ended range
208 for l, h in self.ranges:
211 "Can't determine membership; last value not set")
219 for l, h in self.ranges:
220 l = self.getnext(l-1)
228 if self.ranges and self.ranges[0][0] is None:
229 raise TypeError("Can't iterate; last value not set")
231 return self._iterator()
235 for l, h in self.ranges:
240 raise TypeError("Can't size object; last value not set")
248 for low, high in self.ranges:
255 p.append('%d:*' % (high,))
257 p.append('%d:%d' % (low, high))
261 return '<MessageSet %s>' % (str(self),)
263 def __eq__(self, other):
264 if isinstance(other, MessageSet):
265 return self.ranges == other.ranges
270 def __init__(self, size, defered):
275 def write(self, data):
276 self.size -= len(data)
279 self.data.append(data)
282 data, passon = data[:self.size], data[self.size:]
286 self.data.append(data)
289 def callback(self, line):
291 Call defered with data and rest of line
293 self.defer.callback((''.join(self.data), line))
296 _memoryFileLimit = 1024 * 1024 * 10
298 def __init__(self, size, defered):
301 if size > self._memoryFileLimit:
302 self.data = tempfile.TemporaryFile()
304 self.data = StringIO.StringIO()
306 def write(self, data):
307 self.size -= len(data)
310 self.data.write(data)
313 data, passon = data[:self.size], data[self.size:]
317 self.data.write(data)
320 def callback(self, line):
322 Call defered with data and rest of line
325 self.defer.callback((self.data, line))
329 """Buffer up a bunch of writes before sending them all to a transport at once.
331 def __init__(self, transport, size=8192):
332 self.bufferSize = size
333 self.transport = transport
338 self._length += len(s)
339 self._writes.append(s)
340 if self._length > self.bufferSize:
345 self.transport.writeSequence(self._writes)
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')
356 def __init__(self, command, args=None, wantResponse=(),
357 continuation=None, *contArgs, **contKw):
358 self.command = command
360 self.wantResponse = wantResponse
361 self.continuation = lambda x: continuation(x, *contArgs, **contKw)
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))
369 def finish(self, lastLine, unusedCallback):
373 names = parseNestedParens(L)
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):
381 d, self.defer = self.defer, None
382 d.callback((send, lastLine))
384 unusedCallback(unuse)
386 class LOGINCredentials(cred.credentials.UsernamePassword):
388 self.challenges = ['Password\0', 'User Name\0']
389 self.responses = ['password', 'username']
390 cred.credentials.UsernamePassword.__init__(self, None, None)
392 def getChallenge(self):
393 return self.challenges.pop()
395 def setResponse(self, response):
396 setattr(self, self.responses.pop(), response)
398 def moreChallenges(self):
399 return bool(self.challenges)
401 class PLAINCredentials(cred.credentials.UsernamePassword):
403 cred.credentials.UsernamePassword.__init__(self, None, None)
405 def getChallenge(self):
408 def setResponse(self, response):
409 parts = response.split('\0')
411 raise IllegalClientResponse("Malformed Response - wrong number of parts")
412 useless, self.username, self.password = parts
414 def moreChallenges(self):
417 class IMAP4Exception(Exception):
418 def __init__(self, *args):
419 Exception.__init__(self, *args)
421 class IllegalClientResponse(IMAP4Exception): pass
423 class IllegalOperation(IMAP4Exception): pass
425 class IllegalMailboxEncoding(IMAP4Exception): pass
427 class IMailboxListener(Interface):
428 """Interface for objects interested in mailbox events"""
430 def modeChanged(writeable):
431 """Indicates that the write status of a mailbox has changed.
433 @type writeable: C{bool}
434 @param writeable: A true value if write is now allowed, false
438 def flagsChanged(newFlags):
439 """Indicates that the flags of one or more messages have changed.
441 @type newFlags: C{dict}
442 @param newFlags: A mapping of message identifiers to tuples of flags
443 now set on that message.
446 def newMessages(exists, recent):
447 """Indicates that the number of messages in a mailbox has changed.
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
455 @param recent: The number of messages now flagged \\Recent.
456 If the number of recent messages has not changed, this should be
460 class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin):
462 Protocol implementation for an IMAP4rev1 server.
464 The server can be in any of four states:
470 implements(IMailboxListener)
472 # Identifier for this server software
473 IDENT = 'Twisted IMAP4rev1 Ready'
475 # Number of seconds before idle timeout
476 # Initially 1 minute. Raised to 30 minutes after login.
479 POSTAUTH_TIMEOUT = 60 * 30
481 # Whether STARTTLS has been issued successfully yet or not.
484 # Whether our transport supports TLS
487 # Mapping of tags to commands we have received
490 # The object which will handle logins for us
493 # The account object for this connection
499 # The currently selected mailbox
502 # Command data to be processed when literal data is received
503 _pendingLiteral = None
505 # Maximum length to accept for a "short" string literal
506 _literalStringLimit = 4096
508 # IChallengeResponse factories for AUTHENTICATE command
511 # Search terms the implementation of which needs to be passed both the last
512 # message identifier (UID) and the last sequence id.
513 _requiresLastMessageInfo = set(["OR", "NOT", "UID"])
517 parseState = 'command'
519 def __init__(self, chal = None, contextFactory = None, scheduler = None):
522 self.challengers = chal
523 self.ctx = contextFactory
524 if scheduler is None:
525 scheduler = iterateInReactor
526 self._scheduler = scheduler
527 self._queuedAsync = []
529 def capabilities(self):
530 cap = {'AUTH': self.challengers.keys()}
531 if self.ctx and self.canStartTLS:
532 if not self.startedTLS and interfaces.ISSLTransport(self.transport, None) is None:
533 cap['LOGINDISABLED'] = None
534 cap['STARTTLS'] = None
535 cap['NAMESPACE'] = None
539 def connectionMade(self):
541 self.canStartTLS = interfaces.ITLSTransport(self.transport, None) is not None
542 self.setTimeout(self.timeOut)
543 self.sendServerGreeting()
545 def connectionLost(self, reason):
546 self.setTimeout(None)
549 self._onLogout = None
551 def timeoutConnection(self):
552 self.sendLine('* BYE Autologout; connection idle too long')
553 self.transport.loseConnection()
555 self.mbox.removeListener(self)
556 cmbx = ICloseableMailbox(self.mbox, None)
558 maybeDeferred(cmbx.close).addErrback(log.err)
560 self.state = 'timeout'
562 def rawDataReceived(self, data):
564 passon = self._pendingLiteral.write(data)
565 if passon is not None:
566 self.setLineMode(passon)
568 # Avoid processing commands while buffers are being dumped to
573 commands = self.blocked
575 while commands and self.blocked is None:
576 self.lineReceived(commands.pop(0))
577 if self.blocked is not None:
578 self.blocked.extend(commands)
580 def lineReceived(self, line):
581 if self.blocked is not None:
582 self.blocked.append(line)
587 f = getattr(self, 'parse_' + self.parseState)
591 self.sendUntaggedResponse('BAD Server error: ' + str(e))
594 def parse_command(self, line):
595 args = line.split(None, 2)
598 tag, cmd, rest = args
603 self.sendBadResponse(tag, 'Missing command')
606 self.sendBadResponse(None, 'Null command')
611 return self.dispatchCommand(tag, cmd, rest)
612 except IllegalClientResponse, e:
613 self.sendBadResponse(tag, 'Illegal syntax: ' + str(e))
614 except IllegalOperation, e:
615 self.sendNegativeResponse(tag, 'Illegal operation: ' + str(e))
616 except IllegalMailboxEncoding, e:
617 self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + str(e))
619 def parse_pending(self, line):
620 d = self._pendingLiteral
621 self._pendingLiteral = None
622 self.parseState = 'command'
625 def dispatchCommand(self, tag, cmd, rest, uid=None):
626 f = self.lookupCommand(cmd)
630 self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid)
632 self.sendBadResponse(tag, 'Unsupported command')
634 def lookupCommand(self, cmd):
635 return getattr(self, '_'.join((self.state, cmd.upper())), None)
637 def __doCommand(self, tag, handler, args, parseargs, line, uid):
638 for (i, arg) in enumerate(parseargs):
640 parseargs = parseargs[i+1:]
641 maybeDeferred(arg, self, line).addCallback(
642 self.__cbDispatch, tag, handler, args,
643 parseargs, uid).addErrback(self.__ebDispatch, tag)
650 raise IllegalClientResponse("Too many arguments for command: " + repr(line))
653 handler(uid=uid, *args)
657 def __cbDispatch(self, (arg, rest), tag, fn, args, parseargs, uid):
659 self.__doCommand(tag, fn, args, parseargs, rest, uid)
661 def __ebDispatch(self, failure, tag):
662 if failure.check(IllegalClientResponse):
663 self.sendBadResponse(tag, 'Illegal syntax: ' + str(failure.value))
664 elif failure.check(IllegalOperation):
665 self.sendNegativeResponse(tag, 'Illegal operation: ' +
667 elif failure.check(IllegalMailboxEncoding):
668 self.sendNegativeResponse(tag, 'Illegal mailbox name: ' +
671 self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
674 def _stringLiteral(self, size):
675 if size > self._literalStringLimit:
676 raise IllegalClientResponse(
677 "Literal too long! I accept at most %d octets" %
678 (self._literalStringLimit,))
680 self.parseState = 'pending'
681 self._pendingLiteral = LiteralString(size, d)
682 self.sendContinuationRequest('Ready for %d octets of text' % size)
686 def _fileLiteral(self, size):
688 self.parseState = 'pending'
689 self._pendingLiteral = LiteralFile(size, d)
690 self.sendContinuationRequest('Ready for %d octets of data' % size)
694 def arg_astring(self, line):
696 Parse an astring from the line, return (arg, rest), possibly
697 via a deferred (to handle literals)
701 raise IllegalClientResponse("Missing argument")
703 arg, rest = None, None
706 spam, arg, rest = line.split('"',2)
707 rest = rest[1:] # Strip space
709 raise IllegalClientResponse("Unmatched quotes")
713 raise IllegalClientResponse("Malformed literal")
715 size = int(line[1:-1])
717 raise IllegalClientResponse("Bad literal size: " + line[1:-1])
718 d = self._stringLiteral(size)
720 arg = line.split(' ',1)
724 return d or (arg, rest)
726 # ATOM: Any CHAR except ( ) { % * " \ ] CTL SP (CHAR is 7bit)
727 atomre = re.compile(r'(?P<atom>[^\](){%*"\\\x00-\x20\x80-\xff]+)( (?P<rest>.*$)|$)')
729 def arg_atom(self, line):
731 Parse an atom from the line
734 raise IllegalClientResponse("Missing argument")
735 m = self.atomre.match(line)
737 return m.group('atom'), m.group('rest')
739 raise IllegalClientResponse("Malformed ATOM")
741 def arg_plist(self, line):
743 Parse a (non-nested) parenthesised list from the line
746 raise IllegalClientResponse("Missing argument")
749 raise IllegalClientResponse("Missing parenthesis")
754 raise IllegalClientResponse("Mismatched parenthesis")
756 return (parseNestedParens(line[1:i],0), line[i+2:])
758 def arg_literal(self, line):
760 Parse a literal from the line
763 raise IllegalClientResponse("Missing argument")
766 raise IllegalClientResponse("Missing literal")
769 raise IllegalClientResponse("Malformed literal")
772 size = int(line[1:-1])
774 raise IllegalClientResponse("Bad literal size: " + line[1:-1])
776 return self._fileLiteral(size)
778 def arg_searchkeys(self, line):
782 query = parseNestedParens(line)
783 # XXX Should really use list of search terms and parse into
788 def arg_seqset(self, line):
793 arg = line.split(' ',1)
799 return (parseIdList(arg), rest)
800 except IllegalIdentifierError, e:
801 raise IllegalClientResponse("Bad message number " + str(e))
803 def arg_fetchatt(self, line):
809 return (p.result, '')
811 def arg_flaglist(self, line):
813 Flag part of store-att-flag
818 raise IllegalClientResponse("Mismatched parenthesis")
822 m = self.atomre.search(line)
824 raise IllegalClientResponse("Malformed flag")
825 if line[0] == '\\' and m.start() == 1:
826 flags.append('\\' + m.group('atom'))
828 flags.append(m.group('atom'))
830 raise IllegalClientResponse("Malformed flag")
831 line = m.group('rest')
835 def arg_line(self, line):
837 Command line of UID command
841 def opt_plist(self, line):
843 Optional parenthesised list
845 if line.startswith('('):
846 return self.arg_plist(line)
850 def opt_datetime(self, line):
852 Optional date-time string
854 if line.startswith('"'):
856 spam, date, rest = line.split('"',2)
858 raise IllegalClientResponse("Malformed date-time")
859 return (date, rest[1:])
863 def opt_charset(self, line):
865 Optional charset of SEARCH command
867 if line[:7].upper() == 'CHARSET':
868 arg = line.split(' ',2)
870 raise IllegalClientResponse("Missing charset identifier")
873 spam, arg, rest = arg
878 def sendServerGreeting(self):
879 msg = '[CAPABILITY %s] %s' % (' '.join(self.listCapabilities()), self.IDENT)
880 self.sendPositiveResponse(message=msg)
882 def sendBadResponse(self, tag = None, message = ''):
883 self._respond('BAD', tag, message)
885 def sendPositiveResponse(self, tag = None, message = ''):
886 self._respond('OK', tag, message)
888 def sendNegativeResponse(self, tag = None, message = ''):
889 self._respond('NO', tag, message)
891 def sendUntaggedResponse(self, message, async=False):
892 if not async or (self.blocked is None):
893 self._respond(message, None, None)
895 self._queuedAsync.append(message)
897 def sendContinuationRequest(self, msg = 'Ready for additional command text'):
899 self.sendLine('+ ' + msg)
903 def _respond(self, state, tag, message):
904 if state in ('OK', 'NO', 'BAD') and self._queuedAsync:
905 lines = self._queuedAsync
906 self._queuedAsync = []
908 self._respond(msg, None, None)
912 self.sendLine(' '.join((tag, state, message)))
914 self.sendLine(' '.join((tag, state)))
916 def listCapabilities(self):
918 for c, v in self.capabilities().iteritems():
922 caps.extend([('%s=%s' % (c, cap)) for cap in v])
925 def do_CAPABILITY(self, tag):
926 self.sendUntaggedResponse('CAPABILITY ' + ' '.join(self.listCapabilities()))
927 self.sendPositiveResponse(tag, 'CAPABILITY completed')
929 unauth_CAPABILITY = (do_CAPABILITY,)
930 auth_CAPABILITY = unauth_CAPABILITY
931 select_CAPABILITY = unauth_CAPABILITY
932 logout_CAPABILITY = unauth_CAPABILITY
934 def do_LOGOUT(self, tag):
935 self.sendUntaggedResponse('BYE Nice talking to you')
936 self.sendPositiveResponse(tag, 'LOGOUT successful')
937 self.transport.loseConnection()
939 unauth_LOGOUT = (do_LOGOUT,)
940 auth_LOGOUT = unauth_LOGOUT
941 select_LOGOUT = unauth_LOGOUT
942 logout_LOGOUT = unauth_LOGOUT
944 def do_NOOP(self, tag):
945 self.sendPositiveResponse(tag, 'NOOP No operation performed')
947 unauth_NOOP = (do_NOOP,)
948 auth_NOOP = unauth_NOOP
949 select_NOOP = unauth_NOOP
950 logout_NOOP = unauth_NOOP
952 def do_AUTHENTICATE(self, tag, args):
953 args = args.upper().strip()
954 if args not in self.challengers:
955 self.sendNegativeResponse(tag, 'AUTHENTICATE method unsupported')
957 self.authenticate(self.challengers[args](), tag)
959 unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom)
961 def authenticate(self, chal, tag):
962 if self.portal is None:
963 self.sendNegativeResponse(tag, 'Temporary authentication failure')
966 self._setupChallenge(chal, tag)
968 def _setupChallenge(self, chal, tag):
970 challenge = chal.getChallenge()
972 self.sendBadResponse(tag, 'Server error: ' + str(e))
974 coded = base64.encodestring(challenge)[:-1]
975 self.parseState = 'pending'
976 self._pendingLiteral = defer.Deferred()
977 self.sendContinuationRequest(coded)
978 self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag)
979 self._pendingLiteral.addErrback(self.__ebAuthChunk, tag)
981 def __cbAuthChunk(self, result, chal, tag):
983 uncoded = base64.decodestring(result)
984 except binascii.Error:
985 raise IllegalClientResponse("Malformed Response - not base64")
987 chal.setResponse(uncoded)
988 if chal.moreChallenges():
989 self._setupChallenge(chal, tag)
991 self.portal.login(chal, None, IAccount).addCallbacks(
994 (tag,), None, (tag,), None
997 def __cbAuthResp(self, (iface, avatar, logout), tag):
998 assert iface is IAccount, "IAccount is the only supported interface"
999 self.account = avatar
1001 self._onLogout = logout
1002 self.sendPositiveResponse(tag, 'Authentication successful')
1003 self.setTimeout(self.POSTAUTH_TIMEOUT)
1005 def __ebAuthResp(self, failure, tag):
1006 if failure.check(cred.error.UnauthorizedLogin):
1007 self.sendNegativeResponse(tag, 'Authentication failed: unauthorized')
1008 elif failure.check(cred.error.UnhandledCredentials):
1009 self.sendNegativeResponse(tag, 'Authentication failed: server misconfigured')
1011 self.sendBadResponse(tag, 'Server error: login failed unexpectedly')
1014 def __ebAuthChunk(self, failure, tag):
1015 self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.value))
1017 def do_STARTTLS(self, tag):
1019 self.sendNegativeResponse(tag, 'TLS already negotiated')
1020 elif self.ctx and self.canStartTLS:
1021 self.sendPositiveResponse(tag, 'Begin TLS negotiation now')
1022 self.transport.startTLS(self.ctx)
1023 self.startedTLS = True
1024 self.challengers = self.challengers.copy()
1025 if 'LOGIN' not in self.challengers:
1026 self.challengers['LOGIN'] = LOGINCredentials
1027 if 'PLAIN' not in self.challengers:
1028 self.challengers['PLAIN'] = PLAINCredentials
1030 self.sendNegativeResponse(tag, 'TLS not available')
1032 unauth_STARTTLS = (do_STARTTLS,)
1034 def do_LOGIN(self, tag, user, passwd):
1035 if 'LOGINDISABLED' in self.capabilities():
1036 self.sendBadResponse(tag, 'LOGIN is disabled before STARTTLS')
1039 maybeDeferred(self.authenticateLogin, user, passwd
1040 ).addCallback(self.__cbLogin, tag
1041 ).addErrback(self.__ebLogin, tag
1044 unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring)
1046 def authenticateLogin(self, user, passwd):
1047 """Lookup the account associated with the given parameters
1049 Override this method to define the desired authentication behavior.
1051 The default behavior is to defer authentication to C{self.portal}
1052 if it is not None, or to deny the login otherwise.
1055 @param user: The username to lookup
1057 @type passwd: C{str}
1058 @param passwd: The password to login with
1061 return self.portal.login(
1062 cred.credentials.UsernamePassword(user, passwd),
1065 raise cred.error.UnauthorizedLogin()
1067 def __cbLogin(self, (iface, avatar, logout), tag):
1068 if iface is not IAccount:
1069 self.sendBadResponse(tag, 'Server error: login returned unexpected value')
1070 log.err("__cbLogin called with %r, IAccount expected" % (iface,))
1072 self.account = avatar
1073 self._onLogout = logout
1074 self.sendPositiveResponse(tag, 'LOGIN succeeded')
1076 self.setTimeout(self.POSTAUTH_TIMEOUT)
1078 def __ebLogin(self, failure, tag):
1079 if failure.check(cred.error.UnauthorizedLogin):
1080 self.sendNegativeResponse(tag, 'LOGIN failed')
1082 self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1085 def do_NAMESPACE(self, tag):
1086 personal = public = shared = None
1087 np = INamespacePresenter(self.account, None)
1089 personal = np.getPersonalNamespaces()
1090 public = np.getSharedNamespaces()
1091 shared = np.getSharedNamespaces()
1092 self.sendUntaggedResponse('NAMESPACE ' + collapseNestedLists([personal, public, shared]))
1093 self.sendPositiveResponse(tag, "NAMESPACE command completed")
1095 auth_NAMESPACE = (do_NAMESPACE,)
1096 select_NAMESPACE = auth_NAMESPACE
1098 def _parseMbox(self, name):
1099 if isinstance(name, unicode):
1102 return name.decode('imap4-utf-7')
1105 raise IllegalMailboxEncoding(name)
1107 def _selectWork(self, tag, name, rw, cmdName):
1109 self.mbox.removeListener(self)
1110 cmbx = ICloseableMailbox(self.mbox, None)
1111 if cmbx is not None:
1112 maybeDeferred(cmbx.close).addErrback(log.err)
1116 name = self._parseMbox(name)
1117 maybeDeferred(self.account.select, self._parseMbox(name), rw
1118 ).addCallback(self._cbSelectWork, cmdName, tag
1119 ).addErrback(self._ebSelectWork, cmdName, tag
1122 def _ebSelectWork(self, failure, cmdName, tag):
1123 self.sendBadResponse(tag, "%s failed: Server error" % (cmdName,))
1126 def _cbSelectWork(self, mbox, cmdName, tag):
1128 self.sendNegativeResponse(tag, 'No such mailbox')
1130 if '\\noselect' in [s.lower() for s in mbox.getFlags()]:
1131 self.sendNegativeResponse(tag, 'Mailbox cannot be selected')
1134 flags = mbox.getFlags()
1135 self.sendUntaggedResponse(str(mbox.getMessageCount()) + ' EXISTS')
1136 self.sendUntaggedResponse(str(mbox.getRecentCount()) + ' RECENT')
1137 self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags))
1138 self.sendPositiveResponse(None, '[UIDVALIDITY %d]' % mbox.getUIDValidity())
1140 s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY'
1141 mbox.addListener(self)
1142 self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName))
1143 self.state = 'select'
1146 auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' )
1147 select_SELECT = auth_SELECT
1149 auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' )
1150 select_EXAMINE = auth_EXAMINE
1153 def do_IDLE(self, tag):
1154 self.sendContinuationRequest(None)
1156 self.lastState = self.parseState
1157 self.parseState = 'idle'
1159 def parse_idle(self, *args):
1160 self.parseState = self.lastState
1162 self.sendPositiveResponse(self.parseTag, "IDLE terminated")
1165 select_IDLE = ( do_IDLE, )
1166 auth_IDLE = select_IDLE
1169 def do_CREATE(self, tag, name):
1170 name = self._parseMbox(name)
1172 result = self.account.create(name)
1173 except MailboxException, c:
1174 self.sendNegativeResponse(tag, str(c))
1176 self.sendBadResponse(tag, "Server error encountered while creating mailbox")
1180 self.sendPositiveResponse(tag, 'Mailbox created')
1182 self.sendNegativeResponse(tag, 'Mailbox not created')
1184 auth_CREATE = (do_CREATE, arg_astring)
1185 select_CREATE = auth_CREATE
1187 def do_DELETE(self, tag, name):
1188 name = self._parseMbox(name)
1189 if name.lower() == 'inbox':
1190 self.sendNegativeResponse(tag, 'You cannot delete the inbox')
1193 self.account.delete(name)
1194 except MailboxException, m:
1195 self.sendNegativeResponse(tag, str(m))
1197 self.sendBadResponse(tag, "Server error encountered while deleting mailbox")
1200 self.sendPositiveResponse(tag, 'Mailbox deleted')
1202 auth_DELETE = (do_DELETE, arg_astring)
1203 select_DELETE = auth_DELETE
1205 def do_RENAME(self, tag, oldname, newname):
1206 oldname, newname = [self._parseMbox(n) for n in oldname, newname]
1207 if oldname.lower() == 'inbox' or newname.lower() == 'inbox':
1208 self.sendNegativeResponse(tag, 'You cannot rename the inbox, or rename another mailbox to inbox.')
1211 self.account.rename(oldname, newname)
1213 self.sendBadResponse(tag, 'Invalid command syntax')
1214 except MailboxException, m:
1215 self.sendNegativeResponse(tag, str(m))
1217 self.sendBadResponse(tag, "Server error encountered while renaming mailbox")
1220 self.sendPositiveResponse(tag, 'Mailbox renamed')
1222 auth_RENAME = (do_RENAME, arg_astring, arg_astring)
1223 select_RENAME = auth_RENAME
1225 def do_SUBSCRIBE(self, tag, name):
1226 name = self._parseMbox(name)
1228 self.account.subscribe(name)
1229 except MailboxException, m:
1230 self.sendNegativeResponse(tag, str(m))
1232 self.sendBadResponse(tag, "Server error encountered while subscribing to mailbox")
1235 self.sendPositiveResponse(tag, 'Subscribed')
1237 auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring)
1238 select_SUBSCRIBE = auth_SUBSCRIBE
1240 def do_UNSUBSCRIBE(self, tag, name):
1241 name = self._parseMbox(name)
1243 self.account.unsubscribe(name)
1244 except MailboxException, m:
1245 self.sendNegativeResponse(tag, str(m))
1247 self.sendBadResponse(tag, "Server error encountered while unsubscribing from mailbox")
1250 self.sendPositiveResponse(tag, 'Unsubscribed')
1252 auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring)
1253 select_UNSUBSCRIBE = auth_UNSUBSCRIBE
1255 def _listWork(self, tag, ref, mbox, sub, cmdName):
1256 mbox = self._parseMbox(mbox)
1257 maybeDeferred(self.account.listMailboxes, ref, mbox
1258 ).addCallback(self._cbListWork, tag, sub, cmdName
1259 ).addErrback(self._ebListWork, tag
1262 def _cbListWork(self, mailboxes, tag, sub, cmdName):
1263 for (name, box) in mailboxes:
1264 if not sub or self.account.isSubscribed(name):
1265 flags = box.getFlags()
1266 delim = box.getHierarchicalDelimiter()
1267 resp = (DontQuoteMe(cmdName), map(DontQuoteMe, flags), delim, name.encode('imap4-utf-7'))
1268 self.sendUntaggedResponse(collapseNestedLists(resp))
1269 self.sendPositiveResponse(tag, '%s completed' % (cmdName,))
1271 def _ebListWork(self, failure, tag):
1272 self.sendBadResponse(tag, "Server error encountered while listing mailboxes.")
1275 auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST')
1276 select_LIST = auth_LIST
1278 auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB')
1279 select_LSUB = auth_LSUB
1281 def do_STATUS(self, tag, mailbox, names):
1282 mailbox = self._parseMbox(mailbox)
1283 maybeDeferred(self.account.select, mailbox, 0
1284 ).addCallback(self._cbStatusGotMailbox, tag, mailbox, names
1285 ).addErrback(self._ebStatusGotMailbox, tag
1288 def _cbStatusGotMailbox(self, mbox, tag, mailbox, names):
1290 maybeDeferred(mbox.requestStatus, names).addCallbacks(
1291 self.__cbStatus, self.__ebStatus,
1292 (tag, mailbox), None, (tag, mailbox), None
1295 self.sendNegativeResponse(tag, "Could not open mailbox")
1297 def _ebStatusGotMailbox(self, failure, tag):
1298 self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
1301 auth_STATUS = (do_STATUS, arg_astring, arg_plist)
1302 select_STATUS = auth_STATUS
1304 def __cbStatus(self, status, tag, box):
1305 line = ' '.join(['%s %s' % x for x in status.iteritems()])
1306 self.sendUntaggedResponse('STATUS %s (%s)' % (box, line))
1307 self.sendPositiveResponse(tag, 'STATUS complete')
1309 def __ebStatus(self, failure, tag, box):
1310 self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, str(failure.value)))
1312 def do_APPEND(self, tag, mailbox, flags, date, message):
1313 mailbox = self._parseMbox(mailbox)
1314 maybeDeferred(self.account.select, mailbox
1315 ).addCallback(self._cbAppendGotMailbox, tag, flags, date, message
1316 ).addErrback(self._ebAppendGotMailbox, tag
1319 def _cbAppendGotMailbox(self, mbox, tag, flags, date, message):
1321 self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox')
1324 d = mbox.addMessage(message, flags, date)
1325 d.addCallback(self.__cbAppend, tag, mbox)
1326 d.addErrback(self.__ebAppend, tag)
1328 def _ebAppendGotMailbox(self, failure, tag):
1329 self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
1332 auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime,
1334 select_APPEND = auth_APPEND
1336 def __cbAppend(self, result, tag, mbox):
1337 self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount())
1338 self.sendPositiveResponse(tag, 'APPEND complete')
1340 def __ebAppend(self, failure, tag):
1341 self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value))
1343 def do_CHECK(self, tag):
1344 d = self.checkpoint()
1346 self.__cbCheck(None, tag)
1351 callbackArgs=(tag,),
1354 select_CHECK = (do_CHECK,)
1356 def __cbCheck(self, result, tag):
1357 self.sendPositiveResponse(tag, 'CHECK completed')
1359 def __ebCheck(self, failure, tag):
1360 self.sendBadResponse(tag, 'CHECK failed: ' + str(failure.value))
1362 def checkpoint(self):
1363 """Called when the client issues a CHECK command.
1365 This should perform any checkpoint operations required by the server.
1366 It may be a long running operation, but may not block. If it returns
1367 a deferred, the client will only be informed of success (or failure)
1368 when the deferred's callback (or errback) is invoked.
1372 def do_CLOSE(self, tag):
1374 if self.mbox.isWriteable():
1375 d = maybeDeferred(self.mbox.expunge)
1376 cmbx = ICloseableMailbox(self.mbox, None)
1377 if cmbx is not None:
1379 d.addCallback(lambda result: cmbx.close())
1381 d = maybeDeferred(cmbx.close)
1383 d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,), None)
1385 self.__cbClose(None, tag)
1387 select_CLOSE = (do_CLOSE,)
1389 def __cbClose(self, result, tag):
1390 self.sendPositiveResponse(tag, 'CLOSE completed')
1391 self.mbox.removeListener(self)
1395 def __ebClose(self, failure, tag):
1396 self.sendBadResponse(tag, 'CLOSE failed: ' + str(failure.value))
1398 def do_EXPUNGE(self, tag):
1399 if self.mbox.isWriteable():
1400 maybeDeferred(self.mbox.expunge).addCallbacks(
1401 self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None
1404 self.sendNegativeResponse(tag, 'EXPUNGE ignored on read-only mailbox')
1406 select_EXPUNGE = (do_EXPUNGE,)
1408 def __cbExpunge(self, result, tag):
1410 self.sendUntaggedResponse('%d EXPUNGE' % e)
1411 self.sendPositiveResponse(tag, 'EXPUNGE completed')
1413 def __ebExpunge(self, failure, tag):
1414 self.sendBadResponse(tag, 'EXPUNGE failed: ' + str(failure.value))
1417 def do_SEARCH(self, tag, charset, query, uid=0):
1418 sm = ISearchableMailbox(self.mbox, None)
1420 maybeDeferred(sm.search, query, uid=uid).addCallbacks(
1421 self.__cbSearch, self.__ebSearch,
1422 (tag, self.mbox, uid), None, (tag,), None
1425 # that's not the ideal way to get all messages, there should be a
1426 # method on mailboxes that gives you all of them
1427 s = parseIdList('1:*')
1428 maybeDeferred(self.mbox.fetch, s, uid=uid).addCallbacks(
1429 self.__cbManualSearch, self.__ebSearch,
1430 (tag, self.mbox, query, uid), None, (tag,), None
1433 select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys)
1435 def __cbSearch(self, result, tag, mbox, uid):
1437 result = map(mbox.getUID, result)
1438 ids = ' '.join([str(i) for i in result])
1439 self.sendUntaggedResponse('SEARCH ' + ids)
1440 self.sendPositiveResponse(tag, 'SEARCH completed')
1443 def __cbManualSearch(self, result, tag, mbox, query, uid,
1444 searchResults=None):
1446 Apply the search filter to a set of messages. Send the response to the
1449 @type result: C{list} of C{tuple} of (C{int}, provider of
1451 @param result: A list two tuples of messages with their sequence ids,
1452 sorted by the ids in descending order.
1455 @param tag: A command tag.
1457 @type mbox: Provider of L{imap4.IMailbox}
1458 @param mbox: The searched mailbox.
1460 @type query: C{list}
1461 @param query: A list representing the parsed form of the search query.
1463 @param uid: A flag indicating whether the search is over message
1464 sequence numbers or UIDs.
1466 @type searchResults: C{list}
1467 @param searchResults: The search results so far or C{None} if no
1470 if searchResults is None:
1474 # result is a list of tuples (sequenceId, Message)
1475 lastSequenceId = result and result[-1][0]
1476 lastMessageId = result and result[-1][1].getUID()
1478 for (i, (id, msg)) in zip(range(5), result):
1479 # searchFilter and singleSearchStep will mutate the query. Dang.
1480 # Copy it here or else things will go poorly for subsequent
1482 if self._searchFilter(copy.deepcopy(query), id, msg,
1483 lastSequenceId, lastMessageId):
1485 searchResults.append(str(msg.getUID()))
1487 searchResults.append(str(id))
1489 from twisted.internet import reactor
1491 0, self.__cbManualSearch, result[5:], tag, mbox, query, uid,
1495 self.sendUntaggedResponse('SEARCH ' + ' '.join(searchResults))
1496 self.sendPositiveResponse(tag, 'SEARCH completed')
1499 def _searchFilter(self, query, id, msg, lastSequenceId, lastMessageId):
1501 Pop search terms from the beginning of C{query} until there are none
1502 left and apply them to the given message.
1504 @param query: A list representing the parsed form of the search query.
1506 @param id: The sequence number of the message being checked.
1508 @param msg: The message being checked.
1510 @type lastSequenceId: C{int}
1511 @param lastSequenceId: The highest sequence number of any message in
1512 the mailbox being searched.
1514 @type lastMessageId: C{int}
1515 @param lastMessageId: The highest UID of any message in the mailbox
1518 @return: Boolean indicating whether all of the query terms match the
1522 if not self._singleSearchStep(query, id, msg,
1523 lastSequenceId, lastMessageId):
1528 def _singleSearchStep(self, query, id, msg, lastSequenceId, lastMessageId):
1530 Pop one search term from the beginning of C{query} (possibly more than
1531 one element) and return whether it matches the given message.
1533 @param query: A list representing the parsed form of the search query.
1535 @param id: The sequence number of the message being checked.
1537 @param msg: The message being checked.
1539 @param lastSequenceId: The highest sequence number of any message in
1540 the mailbox being searched.
1542 @param lastMessageId: The highest UID of any message in the mailbox
1545 @return: Boolean indicating whether the query term matched the message.
1549 if isinstance(q, list):
1550 if not self._searchFilter(q, id, msg,
1551 lastSequenceId, lastMessageId):
1555 if not c[:1].isalpha():
1556 # A search term may be a word like ALL, ANSWERED, BCC, etc (see
1557 # below) or it may be a message sequence set. Here we
1558 # recognize a message sequence set "N:M".
1559 messageSet = parseIdList(c, lastSequenceId)
1560 return id in messageSet
1562 f = getattr(self, 'search_' + c)
1564 if c in self._requiresLastMessageInfo:
1565 result = f(query, id, msg, (lastSequenceId,
1568 result = f(query, id, msg)
1573 def search_ALL(self, query, id, msg):
1575 Returns C{True} if the message matches the ALL search key (always).
1577 @type query: A C{list} of C{str}
1578 @param query: A list representing the parsed query string.
1581 @param id: The sequence number of the message being checked.
1583 @type msg: Provider of L{imap4.IMessage}
1587 def search_ANSWERED(self, query, id, msg):
1589 Returns C{True} if the message has been answered.
1591 @type query: A C{list} of C{str}
1592 @param query: A list representing the parsed query string.
1595 @param id: The sequence number of the message being checked.
1597 @type msg: Provider of L{imap4.IMessage}
1599 return '\\Answered' in msg.getFlags()
1601 def search_BCC(self, query, id, msg):
1603 Returns C{True} if the message has a BCC address matching the query.
1605 @type query: A C{list} of C{str}
1606 @param query: A list whose first element is a BCC C{str}
1609 @param id: The sequence number of the message being checked.
1611 @type msg: Provider of L{imap4.IMessage}
1613 bcc = msg.getHeaders(False, 'bcc').get('bcc', '')
1614 return bcc.lower().find(query.pop(0).lower()) != -1
1616 def search_BEFORE(self, query, id, msg):
1617 date = parseTime(query.pop(0))
1618 return rfc822.parsedate(msg.getInternalDate()) < date
1620 def search_BODY(self, query, id, msg):
1621 body = query.pop(0).lower()
1622 return text.strFile(body, msg.getBodyFile(), False)
1624 def search_CC(self, query, id, msg):
1625 cc = msg.getHeaders(False, 'cc').get('cc', '')
1626 return cc.lower().find(query.pop(0).lower()) != -1
1628 def search_DELETED(self, query, id, msg):
1629 return '\\Deleted' in msg.getFlags()
1631 def search_DRAFT(self, query, id, msg):
1632 return '\\Draft' in msg.getFlags()
1634 def search_FLAGGED(self, query, id, msg):
1635 return '\\Flagged' in msg.getFlags()
1637 def search_FROM(self, query, id, msg):
1638 fm = msg.getHeaders(False, 'from').get('from', '')
1639 return fm.lower().find(query.pop(0).lower()) != -1
1641 def search_HEADER(self, query, id, msg):
1642 hdr = query.pop(0).lower()
1643 hdr = msg.getHeaders(False, hdr).get(hdr, '')
1644 return hdr.lower().find(query.pop(0).lower()) != -1
1646 def search_KEYWORD(self, query, id, msg):
1650 def search_LARGER(self, query, id, msg):
1651 return int(query.pop(0)) < msg.getSize()
1653 def search_NEW(self, query, id, msg):
1654 return '\\Recent' in msg.getFlags() and '\\Seen' not in msg.getFlags()
1656 def search_NOT(self, query, id, msg, (lastSequenceId, lastMessageId)):
1658 Returns C{True} if the message does not match the query.
1660 @type query: A C{list} of C{str}
1661 @param query: A list representing the parsed form of the search query.
1664 @param id: The sequence number of the message being checked.
1666 @type msg: Provider of L{imap4.IMessage}
1667 @param msg: The message being checked.
1669 @type lastSequenceId: C{int}
1670 @param lastSequenceId: The highest sequence number of a message in the
1673 @type lastMessageId: C{int}
1674 @param lastMessageId: The highest UID of a message in the mailbox.
1676 return not self._singleSearchStep(query, id, msg,
1677 lastSequenceId, lastMessageId)
1679 def search_OLD(self, query, id, msg):
1680 return '\\Recent' not in msg.getFlags()
1682 def search_ON(self, query, id, msg):
1683 date = parseTime(query.pop(0))
1684 return rfc822.parsedate(msg.getInternalDate()) == date
1686 def search_OR(self, query, id, msg, (lastSequenceId, lastMessageId)):
1688 Returns C{True} if the message matches any of the first two query
1691 @type query: A C{list} of C{str}
1692 @param query: A list representing the parsed form of the search query.
1695 @param id: The sequence number of the message being checked.
1697 @type msg: Provider of L{imap4.IMessage}
1698 @param msg: The message being checked.
1700 @type lastSequenceId: C{int}
1701 @param lastSequenceId: The highest sequence number of a message in the
1704 @type lastMessageId: C{int}
1705 @param lastMessageId: The highest UID of a message in the mailbox.
1707 a = self._singleSearchStep(query, id, msg,
1708 lastSequenceId, lastMessageId)
1709 b = self._singleSearchStep(query, id, msg,
1710 lastSequenceId, lastMessageId)
1713 def search_RECENT(self, query, id, msg):
1714 return '\\Recent' in msg.getFlags()
1716 def search_SEEN(self, query, id, msg):
1717 return '\\Seen' in msg.getFlags()
1719 def search_SENTBEFORE(self, query, id, msg):
1721 Returns C{True} if the message date is earlier than the query date.
1723 @type query: A C{list} of C{str}
1724 @param query: A list whose first element starts with a stringified date
1725 that is a fragment of an L{imap4.Query()}. The date must be in the
1726 format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
1729 @param id: The sequence number of the message being checked.
1731 @type msg: Provider of L{imap4.IMessage}
1733 date = msg.getHeaders(False, 'date').get('date', '')
1734 date = rfc822.parsedate(date)
1735 return date < parseTime(query.pop(0))
1737 def search_SENTON(self, query, id, msg):
1739 Returns C{True} if the message date is the same as the query date.
1741 @type query: A C{list} of C{str}
1742 @param query: A list whose first element starts with a stringified date
1743 that is a fragment of an L{imap4.Query()}. The date must be in the
1744 format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
1746 @type msg: Provider of L{imap4.IMessage}
1748 date = msg.getHeaders(False, 'date').get('date', '')
1749 date = rfc822.parsedate(date)
1750 return date[:3] == parseTime(query.pop(0))[:3]
1752 def search_SENTSINCE(self, query, id, msg):
1754 Returns C{True} if the message date is later than the query date.
1756 @type query: A C{list} of C{str}
1757 @param query: A list whose first element starts with a stringified date
1758 that is a fragment of an L{imap4.Query()}. The date must be in the
1759 format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
1761 @type msg: Provider of L{imap4.IMessage}
1763 date = msg.getHeaders(False, 'date').get('date', '')
1764 date = rfc822.parsedate(date)
1765 return date > parseTime(query.pop(0))
1767 def search_SINCE(self, query, id, msg):
1768 date = parseTime(query.pop(0))
1769 return rfc822.parsedate(msg.getInternalDate()) > date
1771 def search_SMALLER(self, query, id, msg):
1772 return int(query.pop(0)) > msg.getSize()
1774 def search_SUBJECT(self, query, id, msg):
1775 subj = msg.getHeaders(False, 'subject').get('subject', '')
1776 return subj.lower().find(query.pop(0).lower()) != -1
1778 def search_TEXT(self, query, id, msg):
1779 # XXX - This must search headers too
1780 body = query.pop(0).lower()
1781 return text.strFile(body, msg.getBodyFile(), False)
1783 def search_TO(self, query, id, msg):
1784 to = msg.getHeaders(False, 'to').get('to', '')
1785 return to.lower().find(query.pop(0).lower()) != -1
1787 def search_UID(self, query, id, msg, (lastSequenceId, lastMessageId)):
1789 Returns C{True} if the message UID is in the range defined by the
1792 @type query: A C{list} of C{str}
1793 @param query: A list representing the parsed form of the search
1794 query. Its first element should be a C{str} that can be interpreted
1795 as a sequence range, for example '2:4,5:*'.
1798 @param id: The sequence number of the message being checked.
1800 @type msg: Provider of L{imap4.IMessage}
1801 @param msg: The message being checked.
1803 @type lastSequenceId: C{int}
1804 @param lastSequenceId: The highest sequence number of a message in the
1807 @type lastMessageId: C{int}
1808 @param lastMessageId: The highest UID of a message in the mailbox.
1811 m = parseIdList(c, lastMessageId)
1812 return msg.getUID() in m
1814 def search_UNANSWERED(self, query, id, msg):
1815 return '\\Answered' not in msg.getFlags()
1817 def search_UNDELETED(self, query, id, msg):
1818 return '\\Deleted' not in msg.getFlags()
1820 def search_UNDRAFT(self, query, id, msg):
1821 return '\\Draft' not in msg.getFlags()
1823 def search_UNFLAGGED(self, query, id, msg):
1824 return '\\Flagged' not in msg.getFlags()
1826 def search_UNKEYWORD(self, query, id, msg):
1830 def search_UNSEEN(self, query, id, msg):
1831 return '\\Seen' not in msg.getFlags()
1833 def __ebSearch(self, failure, tag):
1834 self.sendBadResponse(tag, 'SEARCH failed: ' + str(failure.value))
1837 def do_FETCH(self, tag, messages, query, uid=0):
1839 self._oldTimeout = self.setTimeout(None)
1840 maybeDeferred(self.mbox.fetch, messages, uid=uid
1842 ).addCallback(self.__cbFetch, tag, query, uid
1843 ).addErrback(self.__ebFetch, tag
1846 self.sendPositiveResponse(tag, 'FETCH complete')
1848 select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt)
1850 def __cbFetch(self, results, tag, query, uid):
1851 if self.blocked is None:
1854 id, msg = results.next()
1855 except StopIteration:
1856 # The idle timeout was suspended while we delivered results,
1858 self.setTimeout(self._oldTimeout)
1859 del self._oldTimeout
1861 # All results have been processed, deliver completion notification.
1863 # It's important to run this *after* resetting the timeout to "rig
1864 # a race" in some test code. writing to the transport will
1865 # synchronously call test code, which synchronously loses the
1866 # connection, calling our connectionLost method, which cancels the
1867 # timeout. We want to make sure that timeout is cancelled *after*
1868 # we reset it above, so that the final state is no timed
1869 # calls. This avoids reactor uncleanliness errors in the test
1871 # XXX: Perhaps loopback should be fixed to not call the user code
1872 # synchronously in transport.write?
1873 self.sendPositiveResponse(tag, 'FETCH completed')
1875 # Instance state is now consistent again (ie, it is as though
1876 # the fetch command never ran), so allow any pending blocked
1877 # commands to execute.
1880 self.spewMessage(id, msg, query, uid
1881 ).addCallback(lambda _: self.__cbFetch(results, tag, query, uid)
1882 ).addErrback(self.__ebSpewMessage
1885 def __ebSpewMessage(self, failure):
1886 # This indicates a programming error.
1887 # There's no reliable way to indicate anything to the client, since we
1888 # may have already written an arbitrary amount of data in response to
1891 self.transport.loseConnection()
1893 def spew_envelope(self, id, msg, _w=None, _f=None):
1895 _w = self.transport.write
1896 _w('ENVELOPE ' + collapseNestedLists([getEnvelope(msg)]))
1898 def spew_flags(self, id, msg, _w=None, _f=None):
1900 _w = self.transport.write
1901 _w('FLAGS ' + '(%s)' % (' '.join(msg.getFlags())))
1903 def spew_internaldate(self, id, msg, _w=None, _f=None):
1905 _w = self.transport.write
1906 idate = msg.getInternalDate()
1907 ttup = rfc822.parsedate_tz(idate)
1909 log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate))
1910 raise IMAP4Exception("Internal failure generating INTERNALDATE")
1912 # need to specify the month manually, as strftime depends on locale
1913 strdate = time.strftime("%d-%%s-%Y %H:%M:%S ", ttup[:9])
1914 odate = strdate % (_MONTH_NAMES[ttup[1]],)
1916 odate = odate + "+0000"
1922 odate = odate + sign + str(((abs(ttup[9]) / 3600) * 100 + (abs(ttup[9]) % 3600) / 60)).zfill(4)
1923 _w('INTERNALDATE ' + _quote(odate))
1925 def spew_rfc822header(self, id, msg, _w=None, _f=None):
1927 _w = self.transport.write
1928 hdrs = _formatHeaders(msg.getHeaders(True))
1929 _w('RFC822.HEADER ' + _literal(hdrs))
1931 def spew_rfc822text(self, id, msg, _w=None, _f=None):
1933 _w = self.transport.write
1936 return FileProducer(msg.getBodyFile()
1937 ).beginProducing(self.transport
1940 def spew_rfc822size(self, id, msg, _w=None, _f=None):
1942 _w = self.transport.write
1943 _w('RFC822.SIZE ' + str(msg.getSize()))
1945 def spew_rfc822(self, id, msg, _w=None, _f=None):
1947 _w = self.transport.write
1950 mf = IMessageFile(msg, None)
1952 return FileProducer(mf.open()
1953 ).beginProducing(self.transport
1955 return MessageProducer(msg, None, self._scheduler
1956 ).beginProducing(self.transport
1959 def spew_uid(self, id, msg, _w=None, _f=None):
1961 _w = self.transport.write
1962 _w('UID ' + str(msg.getUID()))
1964 def spew_bodystructure(self, id, msg, _w=None, _f=None):
1965 _w('BODYSTRUCTURE ' + collapseNestedLists([getBodyStructure(msg, True)]))
1967 def spew_body(self, part, id, msg, _w=None, _f=None):
1969 _w = self.transport.write
1971 if msg.isMultipart():
1972 msg = msg.getSubPart(p)
1974 # Non-multipart messages have an implicit first part but no
1975 # other parts - reject any request for any other part.
1976 raise TypeError("Requested subpart of non-multipart message")
1979 hdrs = msg.getHeaders(part.header.negate, *part.header.fields)
1980 hdrs = _formatHeaders(hdrs)
1981 _w(str(part) + ' ' + _literal(hdrs))
1985 return FileProducer(msg.getBodyFile()
1986 ).beginProducing(self.transport
1989 hdrs = _formatHeaders(msg.getHeaders(True))
1990 _w(str(part) + ' ' + _literal(hdrs))
1995 return FileProducer(msg.getBodyFile()
1996 ).beginProducing(self.transport
1999 mf = IMessageFile(msg, None)
2001 return FileProducer(mf.open()).beginProducing(self.transport)
2002 return MessageProducer(msg, None, self._scheduler).beginProducing(self.transport)
2005 _w('BODY ' + collapseNestedLists([getBodyStructure(msg)]))
2007 def spewMessage(self, id, msg, query, uid):
2008 wbuf = WriteBuffer(self.transport)
2012 write('* %d FETCH (' % (id,))
2022 if part.type == 'uid':
2024 if part.type == 'body':
2025 yield self.spew_body(part, id, msg, write, flush)
2027 f = getattr(self, 'spew_' + part.type)
2028 yield f(id, msg, write, flush)
2029 if part is not query[-1]:
2031 if uid and not seenUID:
2033 yield self.spew_uid(id, msg, write, flush)
2036 return self._scheduler(spew())
2038 def __ebFetch(self, failure, tag):
2039 self.setTimeout(self._oldTimeout)
2040 del self._oldTimeout
2042 self.sendBadResponse(tag, 'FETCH failed: ' + str(failure.value))
2044 def do_STORE(self, tag, messages, mode, flags, uid=0):
2046 silent = mode.endswith('SILENT')
2047 if mode.startswith('+'):
2049 elif mode.startswith('-'):
2054 maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallbacks(
2055 self.__cbStore, self.__ebStore, (tag, self.mbox, uid, silent), None, (tag,), None
2058 select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist)
2060 def __cbStore(self, result, tag, mbox, uid, silent):
2061 if result and not silent:
2062 for (k, v) in result.iteritems():
2064 uidstr = ' UID %d' % mbox.getUID(k)
2067 self.sendUntaggedResponse('%d FETCH (FLAGS (%s)%s)' %
2068 (k, ' '.join(v), uidstr))
2069 self.sendPositiveResponse(tag, 'STORE completed')
2071 def __ebStore(self, failure, tag):
2072 self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
2074 def do_COPY(self, tag, messages, mailbox, uid=0):
2075 mailbox = self._parseMbox(mailbox)
2076 maybeDeferred(self.account.select, mailbox
2077 ).addCallback(self._cbCopySelectedMailbox, tag, messages, mailbox, uid
2078 ).addErrback(self._ebCopySelectedMailbox, tag
2080 select_COPY = (do_COPY, arg_seqset, arg_astring)
2082 def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid):
2084 self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox)
2086 maybeDeferred(self.mbox.fetch, messages, uid
2087 ).addCallback(self.__cbCopy, tag, mbox
2088 ).addCallback(self.__cbCopied, tag, mbox
2089 ).addErrback(self.__ebCopy, tag
2092 def _ebCopySelectedMailbox(self, failure, tag):
2093 self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
2095 def __cbCopy(self, messages, tag, mbox):
2096 # XXX - This should handle failures with a rollback or something
2101 fastCopyMbox = IMessageCopier(mbox, None)
2102 for (id, msg) in messages:
2103 if fastCopyMbox is not None:
2104 d = maybeDeferred(fastCopyMbox.copy, msg)
2105 addedDeferreds.append(d)
2108 # XXX - The following should be an implementation of IMessageCopier.copy
2109 # on an IMailbox->IMessageCopier adapter.
2111 flags = msg.getFlags()
2112 date = msg.getInternalDate()
2114 body = IMessageFile(msg, None)
2115 if body is not None:
2116 bodyFile = body.open()
2117 d = maybeDeferred(mbox.addMessage, bodyFile, flags, date)
2122 buffer = tempfile.TemporaryFile()
2123 d = MessageProducer(msg, buffer, self._scheduler
2124 ).beginProducing(None
2125 ).addCallback(lambda _, b=buffer, f=flags, d=date: mbox.addMessage(rewind(b), f, d)
2127 addedDeferreds.append(d)
2128 return defer.DeferredList(addedDeferreds)
2130 def __cbCopied(self, deferredIds, tag, mbox):
2133 for (status, result) in deferredIds:
2137 failures.append(result.value)
2139 self.sendNegativeResponse(tag, '[ALERT] Some messages were not copied')
2141 self.sendPositiveResponse(tag, 'COPY completed')
2143 def __ebCopy(self, failure, tag):
2144 self.sendBadResponse(tag, 'COPY failed:' + str(failure.value))
2147 def do_UID(self, tag, command, line):
2148 command = command.upper()
2150 if command not in ('COPY', 'FETCH', 'STORE', 'SEARCH'):
2151 raise IllegalClientResponse(command)
2153 self.dispatchCommand(tag, command, line, uid=1)
2155 select_UID = (do_UID, arg_atom, arg_line)
2157 # IMailboxListener implementation
2159 def modeChanged(self, writeable):
2161 self.sendUntaggedResponse(message='[READ-WRITE]', async=True)
2163 self.sendUntaggedResponse(message='[READ-ONLY]', async=True)
2165 def flagsChanged(self, newFlags):
2166 for (mId, flags) in newFlags.iteritems():
2167 msg = '%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags))
2168 self.sendUntaggedResponse(msg, async=True)
2170 def newMessages(self, exists, recent):
2171 if exists is not None:
2172 self.sendUntaggedResponse('%d EXISTS' % exists, async=True)
2173 if recent is not None:
2174 self.sendUntaggedResponse('%d RECENT' % recent, async=True)
2177 class UnhandledResponse(IMAP4Exception): pass
2179 class NegativeResponse(IMAP4Exception): pass
2181 class NoSupportedAuthentication(IMAP4Exception):
2182 def __init__(self, serverSupports, clientSupports):
2183 IMAP4Exception.__init__(self, 'No supported authentication schemes available')
2184 self.serverSupports = serverSupports
2185 self.clientSupports = clientSupports
2188 return (IMAP4Exception.__str__(self)
2189 + ': Server supports %r, client supports %r'
2190 % (self.serverSupports, self.clientSupports))
2192 class IllegalServerResponse(IMAP4Exception): pass
2194 TIMEOUT_ERROR = error.TimeoutError()
2196 class IMAP4Client(basic.LineReceiver, policies.TimeoutMixin):
2197 """IMAP4 client protocol implementation
2199 @ivar state: A string representing the state the connection is currently
2202 implements(IMailboxListener)
2212 # Number of seconds to wait before timing out a connection.
2213 # If the number is <= 0 no timeout checking will be performed.
2216 # Capabilities are not allowed to change during the session
2217 # So cache the first response and use that for all later
2221 _memoryFileLimit = 1024 * 1024 * 10
2223 # Authentication is pluggable. This maps names to IClientAuthentication
2225 authenticators = None
2227 STATUS_CODES = ('OK', 'NO', 'BAD', 'PREAUTH', 'BYE')
2229 STATUS_TRANSFORMATIONS = {
2230 'MESSAGES': int, 'RECENT': int, 'UNSEEN': int
2235 def __init__(self, contextFactory = None):
2238 self.authenticators = {}
2239 self.context = contextFactory
2243 self._lastCmd = None
2245 def registerAuthenticator(self, auth):
2246 """Register a new form of authentication
2248 When invoking the authenticate() method of IMAP4Client, the first
2249 matching authentication scheme found will be used. The ordering is
2250 that in which the server lists support authentication schemes.
2252 @type auth: Implementor of C{IClientAuthentication}
2253 @param auth: The object to use to perform the client
2254 side of this authentication scheme.
2256 self.authenticators[auth.getName().upper()] = auth
2258 def rawDataReceived(self, data):
2259 if self.timeout > 0:
2262 self._pendingSize -= len(data)
2263 if self._pendingSize > 0:
2264 self._pendingBuffer.write(data)
2267 if self._pendingSize < 0:
2268 data, passon = data[:self._pendingSize], data[self._pendingSize:]
2269 self._pendingBuffer.write(data)
2270 rest = self._pendingBuffer
2271 self._pendingBuffer = None
2272 self._pendingSize = None
2274 self._parts.append(rest.read())
2275 self.setLineMode(passon.lstrip('\r\n'))
2277 # def sendLine(self, line):
2278 # print 'S:', repr(line)
2279 # return basic.LineReceiver.sendLine(self, line)
2281 def _setupForLiteral(self, rest, octets):
2282 self._pendingBuffer = self.messageFile(octets)
2283 self._pendingSize = octets
2284 if self._parts is None:
2285 self._parts = [rest, '\r\n']
2287 self._parts.extend([rest, '\r\n'])
2290 def connectionMade(self):
2291 if self.timeout > 0:
2292 self.setTimeout(self.timeout)
2294 def connectionLost(self, reason):
2295 """We are no longer connected"""
2296 if self.timeout > 0:
2297 self.setTimeout(None)
2298 if self.queued is not None:
2299 queued = self.queued
2302 cmd.defer.errback(reason)
2303 if self.tags is not None:
2306 for cmd in tags.itervalues():
2307 if cmd is not None and cmd.defer is not None:
2308 cmd.defer.errback(reason)
2311 def lineReceived(self, line):
2313 Attempt to parse a single line from the server.
2316 @param line: The line from the server, without the line delimiter.
2318 @raise IllegalServerResponse: If the line or some part of the line
2319 does not represent an allowed message from the server at this time.
2321 # print 'C: ' + repr(line)
2322 if self.timeout > 0:
2325 lastPart = line.rfind('{')
2327 lastPart = line[lastPart + 1:]
2328 if lastPart.endswith('}'):
2329 # It's a literal a-comin' in
2331 octets = int(lastPart[:-1])
2333 raise IllegalServerResponse(line)
2334 if self._parts is None:
2335 self._tag, parts = line.split(None, 1)
2338 self._setupForLiteral(parts, octets)
2341 if self._parts is None:
2342 # It isn't a literal at all
2343 self._regularDispatch(line)
2345 # If an expression is in progress, no tag is required here
2346 # Since we didn't find a literal indicator, this expression
2348 self._parts.append(line)
2349 tag, rest = self._tag, ''.join(self._parts)
2350 self._tag = self._parts = None
2351 self.dispatchCommand(tag, rest)
2353 def timeoutConnection(self):
2354 if self._lastCmd and self._lastCmd.defer is not None:
2355 d, self._lastCmd.defer = self._lastCmd.defer, None
2356 d.errback(TIMEOUT_ERROR)
2359 for cmd in self.queued:
2360 if cmd.defer is not None:
2361 d, cmd.defer = cmd.defer, d
2362 d.errback(TIMEOUT_ERROR)
2364 self.transport.loseConnection()
2366 def _regularDispatch(self, line):
2367 parts = line.split(None, 1)
2371 self.dispatchCommand(tag, rest)
2373 def messageFile(self, octets):
2374 """Create a file to which an incoming message may be written.
2376 @type octets: C{int}
2377 @param octets: The number of octets which will be written to the file
2379 @rtype: Any object which implements C{write(string)} and
2381 @return: A file-like object
2383 if octets > self._memoryFileLimit:
2384 return tempfile.TemporaryFile()
2386 return StringIO.StringIO()
2389 tag = '%0.4X' % self.tagID
2393 def dispatchCommand(self, tag, rest):
2394 if self.state is None:
2395 f = self.response_UNAUTH
2397 f = getattr(self, 'response_' + self.state.upper(), None)
2403 self.transport.loseConnection()
2405 log.err("Cannot dispatch: %s, %s, %s" % (self.state, tag, rest))
2406 self.transport.loseConnection()
2408 def response_UNAUTH(self, tag, rest):
2409 if self.state is None:
2410 # Server greeting, this is
2411 status, rest = rest.split(None, 1)
2412 if status.upper() == 'OK':
2413 self.state = 'unauth'
2414 elif status.upper() == 'PREAUTH':
2417 # XXX - This is rude.
2418 self.transport.loseConnection()
2419 raise IllegalServerResponse(tag + ' ' + rest)
2421 b, e = rest.find('['), rest.find(']')
2422 if b != -1 and e != -1:
2423 self.serverGreeting(
2424 self.__cbCapabilities(
2425 ([parseNestedParens(rest[b + 1:e])], None)))
2427 self.serverGreeting(None)
2429 self._defaultHandler(tag, rest)
2431 def response_AUTH(self, tag, rest):
2432 self._defaultHandler(tag, rest)
2434 def _defaultHandler(self, tag, rest):
2435 if tag == '*' or tag == '+':
2436 if not self.waiting:
2437 self._extraInfo([parseNestedParens(rest)])
2439 cmd = self.tags[self.waiting]
2441 cmd.continuation(rest)
2443 cmd.lines.append(rest)
2446 cmd = self.tags[tag]
2448 # XXX - This is rude.
2449 self.transport.loseConnection()
2450 raise IllegalServerResponse(tag + ' ' + rest)
2452 status, line = rest.split(None, 1)
2454 # Give them this last line, too
2455 cmd.finish(rest, self._extraInfo)
2457 cmd.defer.errback(IMAP4Exception(line))
2462 def _flushQueue(self):
2464 cmd = self.queued.pop(0)
2467 self.sendLine(cmd.format(t))
2470 def _extraInfo(self, lines):
2471 # XXX - This is terrible.
2472 # XXX - Also, this should collapse temporally proximate calls into single
2473 # invocations of IMailboxListener methods, where possible.
2475 recent = exists = None
2476 for response in lines:
2477 elements = len(response)
2478 if elements == 1 and response[0] == ['READ-ONLY']:
2479 self.modeChanged(False)
2480 elif elements == 1 and response[0] == ['READ-WRITE']:
2481 self.modeChanged(True)
2482 elif elements == 2 and response[1] == 'EXISTS':
2483 exists = int(response[0])
2484 elif elements == 2 and response[1] == 'RECENT':
2485 recent = int(response[0])
2486 elif elements == 3 and response[1] == 'FETCH':
2487 mId = int(response[0])
2488 values = self._parseFetchPairs(response[2])
2489 flags.setdefault(mId, []).extend(values.get('FLAGS', ()))
2491 log.msg('Unhandled unsolicited response: %s' % (response,))
2494 self.flagsChanged(flags)
2495 if recent is not None or exists is not None:
2496 self.newMessages(exists, recent)
2498 def sendCommand(self, cmd):
2499 cmd.defer = defer.Deferred()
2501 self.queued.append(cmd)
2505 self.sendLine(cmd.format(t))
2510 def getCapabilities(self, useCache=1):
2511 """Request the capabilities available on this server.
2513 This command is allowed in any state of connection.
2515 @type useCache: C{bool}
2516 @param useCache: Specify whether to use the capability-cache or to
2517 re-retrieve the capabilities from the server. Server capabilities
2518 should never change, so for normal use, this flag should never be
2522 @return: A deferred whose callback will be invoked with a
2523 dictionary mapping capability types to lists of supported
2524 mechanisms, or to None if a support list is not applicable.
2526 if useCache and self._capCache is not None:
2527 return defer.succeed(self._capCache)
2529 resp = ('CAPABILITY',)
2530 d = self.sendCommand(Command(cmd, wantResponse=resp))
2531 d.addCallback(self.__cbCapabilities)
2534 def __cbCapabilities(self, (lines, tagline)):
2537 for cap in rest[1:]:
2538 parts = cap.split('=', 1)
2540 category, value = parts[0], None
2542 category, value = parts
2543 caps.setdefault(category, []).append(value)
2545 # Preserve a non-ideal API for backwards compatibility. It would
2546 # probably be entirely sensible to have an object with a wider API than
2547 # dict here so this could be presented less insanely.
2548 for category in caps:
2549 if caps[category] == [None]:
2550 caps[category] = None
2551 self._capCache = caps
2555 """Inform the server that we are done with the connection.
2557 This command is allowed in any state of connection.
2560 @return: A deferred whose callback will be invoked with None
2561 when the proper server acknowledgement has been received.
2563 d = self.sendCommand(Command('LOGOUT', wantResponse=('BYE',)))
2564 d.addCallback(self.__cbLogout)
2567 def __cbLogout(self, (lines, tagline)):
2568 self.transport.loseConnection()
2569 # We don't particularly care what the server said
2574 """Perform no operation.
2576 This command is allowed in any state of connection.
2579 @return: A deferred whose callback will be invoked with a list
2580 of untagged status updates the server responds with.
2582 d = self.sendCommand(Command('NOOP'))
2583 d.addCallback(self.__cbNoop)
2586 def __cbNoop(self, (lines, tagline)):
2587 # Conceivable, this is elidable.
2588 # It is, afterall, a no-op.
2591 def startTLS(self, contextFactory=None):
2593 Initiates a 'STARTTLS' request and negotiates the TLS / SSL
2596 @param contextFactory: The TLS / SSL Context Factory to
2597 leverage. If the contextFactory is None the IMAP4Client will
2598 either use the current TLS / SSL Context Factory or attempt to
2601 @type contextFactory: C{ssl.ClientContextFactory}
2603 @return: A Deferred which fires when the transport has been
2604 secured according to the given contextFactory, or which fails
2605 if the transport cannot be secured.
2607 assert not self.startedTLS, "Client and Server are currently communicating via TLS"
2609 if contextFactory is None:
2610 contextFactory = self._getContextFactory()
2612 if contextFactory is None:
2613 return defer.fail(IMAP4Exception(
2614 "IMAP4Client requires a TLS context to "
2615 "initiate the STARTTLS handshake"))
2617 if 'STARTTLS' not in self._capCache:
2618 return defer.fail(IMAP4Exception(
2619 "Server does not support secure communication "
2622 tls = interfaces.ITLSTransport(self.transport, None)
2624 return defer.fail(IMAP4Exception(
2625 "IMAP4Client transport does not implement "
2626 "interfaces.ITLSTransport"))
2628 d = self.sendCommand(Command('STARTTLS'))
2629 d.addCallback(self._startedTLS, contextFactory)
2630 d.addCallback(lambda _: self.getCapabilities())
2634 def authenticate(self, secret):
2635 """Attempt to enter the authenticated state with the server
2637 This command is allowed in the Non-Authenticated state.
2640 @return: A deferred whose callback is invoked if the authentication
2641 succeeds and whose errback will be invoked otherwise.
2643 if self._capCache is None:
2644 d = self.getCapabilities()
2646 d = defer.succeed(self._capCache)
2647 d.addCallback(self.__cbAuthenticate, secret)
2650 def __cbAuthenticate(self, caps, secret):
2651 auths = caps.get('AUTH', ())
2652 for scheme in auths:
2653 if scheme.upper() in self.authenticators:
2654 cmd = Command('AUTHENTICATE', scheme, (),
2655 self.__cbContinueAuth, scheme,
2657 return self.sendCommand(cmd)
2660 return defer.fail(NoSupportedAuthentication(
2661 auths, self.authenticators.keys()))
2663 def ebStartTLS(err):
2664 err.trap(IMAP4Exception)
2665 # We couldn't negotiate TLS for some reason
2666 return defer.fail(NoSupportedAuthentication(
2667 auths, self.authenticators.keys()))
2670 d.addErrback(ebStartTLS)
2671 d.addCallback(lambda _: self.getCapabilities())
2672 d.addCallback(self.__cbAuthTLS, secret)
2676 def __cbContinueAuth(self, rest, scheme, secret):
2678 chal = base64.decodestring(rest + '\n')
2679 except binascii.Error:
2681 raise IllegalServerResponse(rest)
2682 self.transport.loseConnection()
2684 auth = self.authenticators[scheme]
2685 chal = auth.challengeResponse(secret, chal)
2686 self.sendLine(base64.encodestring(chal).strip())
2688 def __cbAuthTLS(self, caps, secret):
2689 auths = caps.get('AUTH', ())
2690 for scheme in auths:
2691 if scheme.upper() in self.authenticators:
2692 cmd = Command('AUTHENTICATE', scheme, (),
2693 self.__cbContinueAuth, scheme,
2695 return self.sendCommand(cmd)
2696 raise NoSupportedAuthentication(auths, self.authenticators.keys())
2699 def login(self, username, password):
2700 """Authenticate with the server using a username and password
2702 This command is allowed in the Non-Authenticated state. If the
2703 server supports the STARTTLS capability and our transport supports
2704 TLS, TLS is negotiated before the login command is issued.
2706 A more secure way to log in is to use C{startTLS} or
2707 C{authenticate} or both.
2709 @type username: C{str}
2710 @param username: The username to log in with
2712 @type password: C{str}
2713 @param password: The password to log in with
2716 @return: A deferred whose callback is invoked if login is successful
2717 and whose errback is invoked otherwise.
2719 d = maybeDeferred(self.getCapabilities)
2720 d.addCallback(self.__cbLoginCaps, username, password)
2723 def serverGreeting(self, caps):
2724 """Called when the server has sent us a greeting.
2727 @param caps: Capabilities the server advertised in its greeting.
2730 def _getContextFactory(self):
2731 if self.context is not None:
2734 from twisted.internet import ssl
2738 context = ssl.ClientContextFactory()
2739 context.method = ssl.SSL.TLSv1_METHOD
2742 def __cbLoginCaps(self, capabilities, username, password):
2743 # If the server advertises STARTTLS, we might want to try to switch to TLS
2744 tryTLS = 'STARTTLS' in capabilities
2746 # If our transport supports switching to TLS, we might want to try to switch to TLS.
2747 tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None
2749 # If our transport is not already using TLS, we might want to try to switch to TLS.
2750 nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None
2752 if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport:
2758 callbackArgs=(username, password),
2763 log.msg("Server has no TLS support. logging in over cleartext!")
2764 args = ' '.join((_quote(username), _quote(password)))
2765 return self.sendCommand(Command('LOGIN', args))
2767 def _startedTLS(self, result, context):
2768 self.transport.startTLS(context)
2769 self._capCache = None
2770 self.startedTLS = True
2773 def __cbLoginTLS(self, result, username, password):
2774 args = ' '.join((_quote(username), _quote(password)))
2775 return self.sendCommand(Command('LOGIN', args))
2777 def __ebLoginTLS(self, failure):
2781 def namespace(self):
2782 """Retrieve information about the namespaces available to this account
2784 This command is allowed in the Authenticated and Selected states.
2787 @return: A deferred whose callback is invoked with namespace
2788 information. An example of this information is::
2790 [[['', '/']], [], []]
2792 which indicates a single personal namespace called '' with '/'
2793 as its hierarchical delimiter, and no shared or user namespaces.
2796 resp = ('NAMESPACE',)
2797 d = self.sendCommand(Command(cmd, wantResponse=resp))
2798 d.addCallback(self.__cbNamespace)
2801 def __cbNamespace(self, (lines, last)):
2803 if len(parts) == 4 and parts[0] == 'NAMESPACE':
2804 return [e or [] for e in parts[1:]]
2805 log.err("No NAMESPACE response to NAMESPACE command")
2809 def select(self, mailbox):
2813 This command is allowed in the Authenticated and Selected states.
2815 @type mailbox: C{str}
2816 @param mailbox: The name of the mailbox to select
2819 @return: A deferred whose callback is invoked with mailbox
2820 information if the select is successful and whose errback is
2821 invoked otherwise. Mailbox information consists of a dictionary
2822 with the following keys and values::
2824 FLAGS: A list of strings containing the flags settable on
2825 messages in this mailbox.
2827 EXISTS: An integer indicating the number of messages in this
2830 RECENT: An integer indicating the number of "recent"
2831 messages in this mailbox.
2833 UNSEEN: The message sequence number (an integer) of the
2834 first unseen message in the mailbox.
2836 PERMANENTFLAGS: A list of strings containing the flags that
2837 can be permanently set on messages in this mailbox.
2839 UIDVALIDITY: An integer uniquely identifying this mailbox.
2842 args = _prepareMailboxName(mailbox)
2843 resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
2844 d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2845 d.addCallback(self.__cbSelect, 1)
2849 def examine(self, mailbox):
2850 """Select a mailbox in read-only mode
2852 This command is allowed in the Authenticated and Selected states.
2854 @type mailbox: C{str}
2855 @param mailbox: The name of the mailbox to examine
2858 @return: A deferred whose callback is invoked with mailbox
2859 information if the examine is successful and whose errback
2860 is invoked otherwise. Mailbox information consists of a dictionary
2861 with the following keys and values::
2863 'FLAGS': A list of strings containing the flags settable on
2864 messages in this mailbox.
2866 'EXISTS': An integer indicating the number of messages in this
2869 'RECENT': An integer indicating the number of \"recent\"
2870 messages in this mailbox.
2872 'UNSEEN': An integer indicating the number of messages not
2873 flagged \\Seen in this mailbox.
2875 'PERMANENTFLAGS': A list of strings containing the flags that
2876 can be permanently set on messages in this mailbox.
2878 'UIDVALIDITY': An integer uniquely identifying this mailbox.
2881 args = _prepareMailboxName(mailbox)
2882 resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
2883 d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2884 d.addCallback(self.__cbSelect, 0)
2888 def _intOrRaise(self, value, phrase):
2890 Parse C{value} as an integer and return the result or raise
2891 L{IllegalServerResponse} with C{phrase} as an argument if C{value}
2892 cannot be parsed as an integer.
2897 raise IllegalServerResponse(phrase)
2900 def __cbSelect(self, (lines, tagline), rw):
2902 Handle lines received in response to a SELECT or EXAMINE command.
2904 See RFC 3501, section 6.3.1.
2906 # In the absense of specification, we are free to assume:
2908 datum = {'READ-WRITE': rw}
2909 lines.append(parseNestedParens(tagline))
2911 if len(split) > 0 and split[0].upper() == 'OK':
2912 # Handle all the kinds of OK response.
2914 key = content[0].upper()
2915 if key == 'READ-ONLY':
2916 datum['READ-WRITE'] = False
2917 elif key == 'READ-WRITE':
2918 datum['READ-WRITE'] = True
2919 elif key == 'UIDVALIDITY':
2920 datum['UIDVALIDITY'] = self._intOrRaise(
2922 elif key == 'UNSEEN':
2923 datum['UNSEEN'] = self._intOrRaise(content[1], split)
2924 elif key == 'UIDNEXT':
2925 datum['UIDNEXT'] = self._intOrRaise(content[1], split)
2926 elif key == 'PERMANENTFLAGS':
2927 datum['PERMANENTFLAGS'] = tuple(content[1])
2929 log.err('Unhandled SELECT response (2): %s' % (split,))
2930 elif len(split) == 2:
2931 # Handle FLAGS, EXISTS, and RECENT
2932 if split[0].upper() == 'FLAGS':
2933 datum['FLAGS'] = tuple(split[1])
2934 elif isinstance(split[1], str):
2935 # Must make sure things are strings before treating them as
2936 # strings since some other forms of response have nesting in
2937 # places which results in lists instead.
2938 if split[1].upper() == 'EXISTS':
2939 datum['EXISTS'] = self._intOrRaise(split[0], split)
2940 elif split[1].upper() == 'RECENT':
2941 datum['RECENT'] = self._intOrRaise(split[0], split)
2943 log.err('Unhandled SELECT response (0): %s' % (split,))
2945 log.err('Unhandled SELECT response (1): %s' % (split,))
2947 log.err('Unhandled SELECT response (4): %s' % (split,))
2951 def create(self, name):
2952 """Create a new mailbox on the server
2954 This command is allowed in the Authenticated and Selected states.
2957 @param name: The name of the mailbox to create.
2960 @return: A deferred whose callback is invoked if the mailbox creation
2961 is successful and whose errback is invoked otherwise.
2963 return self.sendCommand(Command('CREATE', _prepareMailboxName(name)))
2965 def delete(self, name):
2968 This command is allowed in the Authenticated and Selected states.
2971 @param name: The name of the mailbox to delete.
2974 @return: A deferred whose calblack is invoked if the mailbox is
2975 deleted successfully and whose errback is invoked otherwise.
2977 return self.sendCommand(Command('DELETE', _prepareMailboxName(name)))
2979 def rename(self, oldname, newname):
2982 This command is allowed in the Authenticated and Selected states.
2984 @type oldname: C{str}
2985 @param oldname: The current name of the mailbox to rename.
2987 @type newname: C{str}
2988 @param newname: The new name to give the mailbox.
2991 @return: A deferred whose callback is invoked if the rename is
2992 successful and whose errback is invoked otherwise.
2994 oldname = _prepareMailboxName(oldname)
2995 newname = _prepareMailboxName(newname)
2996 return self.sendCommand(Command('RENAME', ' '.join((oldname, newname))))
2998 def subscribe(self, name):
2999 """Add a mailbox to the subscription list
3001 This command is allowed in the Authenticated and Selected states.
3004 @param name: The mailbox to mark as 'active' or 'subscribed'
3007 @return: A deferred whose callback is invoked if the subscription
3008 is successful and whose errback is invoked otherwise.
3010 return self.sendCommand(Command('SUBSCRIBE', _prepareMailboxName(name)))
3012 def unsubscribe(self, name):
3013 """Remove a mailbox from the subscription list
3015 This command is allowed in the Authenticated and Selected states.
3018 @param name: The mailbox to unsubscribe
3021 @return: A deferred whose callback is invoked if the unsubscription
3022 is successful and whose errback is invoked otherwise.
3024 return self.sendCommand(Command('UNSUBSCRIBE', _prepareMailboxName(name)))
3026 def list(self, reference, wildcard):
3027 """List a subset of the available mailboxes
3029 This command is allowed in the Authenticated and Selected states.
3031 @type reference: C{str}
3032 @param reference: The context in which to interpret C{wildcard}
3034 @type wildcard: C{str}
3035 @param wildcard: The pattern of mailbox names to match, optionally
3036 including either or both of the '*' and '%' wildcards. '*' will
3037 match zero or more characters and cross hierarchical boundaries.
3038 '%' will also match zero or more characters, but is limited to a
3039 single hierarchical level.
3042 @return: A deferred whose callback is invoked with a list of C{tuple}s,
3043 the first element of which is a C{tuple} of mailbox flags, the second
3044 element of which is the hierarchy delimiter for this mailbox, and the
3045 third of which is the mailbox name; if the command is unsuccessful,
3046 the deferred's errback is invoked instead.
3049 args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
3051 d = self.sendCommand(Command(cmd, args, wantResponse=resp))
3052 d.addCallback(self.__cbList, 'LIST')
3055 def lsub(self, reference, wildcard):
3056 """List a subset of the subscribed available mailboxes
3058 This command is allowed in the Authenticated and Selected states.
3060 The parameters and returned object are the same as for the C{list}
3061 method, with one slight difference: Only mailboxes which have been
3062 subscribed can be included in the resulting list.
3065 args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
3067 d = self.sendCommand(Command(cmd, args, wantResponse=resp))
3068 d.addCallback(self.__cbList, 'LSUB')
3071 def __cbList(self, (lines, last), command):
3074 if len(parts) == 4 and parts[0] == command:
3075 parts[1] = tuple(parts[1])
3076 results.append(tuple(parts[1:]))
3079 def status(self, mailbox, *names):
3081 Retrieve the status of the given mailbox
3083 This command is allowed in the Authenticated and Selected states.
3085 @type mailbox: C{str}
3086 @param mailbox: The name of the mailbox to query
3088 @type *names: C{str}
3089 @param *names: The status names to query. These may be any number of:
3090 C{'MESSAGES'}, C{'RECENT'}, C{'UIDNEXT'}, C{'UIDVALIDITY'}, and
3094 @return: A deferred which fires with with the status information if the
3095 command is successful and whose errback is invoked otherwise. The
3096 status information is in the form of a C{dict}. Each element of
3097 C{names} is a key in the dictionary. The value for each key is the
3098 corresponding response from the server.
3101 args = "%s (%s)" % (_prepareMailboxName(mailbox), ' '.join(names))
3103 d = self.sendCommand(Command(cmd, args, wantResponse=resp))
3104 d.addCallback(self.__cbStatus)
3107 def __cbStatus(self, (lines, last)):
3110 if parts[0] == 'STATUS':
3112 items = [items[i:i+2] for i in range(0, len(items), 2)]
3113 status.update(dict(items))
3114 for k in status.keys():
3115 t = self.STATUS_TRANSFORMATIONS.get(k)
3118 status[k] = t(status[k])
3119 except Exception, e:
3120 raise IllegalServerResponse('(%s %s): %s' % (k, status[k], str(e)))
3123 def append(self, mailbox, message, flags = (), date = None):
3124 """Add the given message to the given mailbox.
3126 This command is allowed in the Authenticated and Selected states.
3128 @type mailbox: C{str}
3129 @param mailbox: The mailbox to which to add this message.
3131 @type message: Any file-like object
3132 @param message: The message to add, in RFC822 format. Newlines
3133 in this file should be \\r\\n-style.
3135 @type flags: Any iterable of C{str}
3136 @param flags: The flags to associated with this message.
3139 @param date: The date to associate with this message. This should
3140 be of the format DD-MM-YYYY HH:MM:SS +/-HHMM. For example, in
3141 Eastern Standard Time, on July 1st 2004 at half past 1 PM,
3142 \"01-07-2004 13:30:00 -0500\".
3145 @return: A deferred whose callback is invoked when this command
3146 succeeds or whose errback is invoked if it fails.
3151 fmt = '%s (%s)%s {%d}'
3153 date = ' "%s"' % date
3157 _prepareMailboxName(mailbox), ' '.join(flags),
3160 d = self.sendCommand(Command('APPEND', cmd, (), self.__cbContinueAppend, message))
3163 def __cbContinueAppend(self, lines, message):
3164 s = basic.FileSender()
3165 return s.beginFileTransfer(message, self.transport, None
3166 ).addCallback(self.__cbFinishAppend)
3168 def __cbFinishAppend(self, foo):
3172 """Tell the server to perform a checkpoint
3174 This command is allowed in the Selected state.
3177 @return: A deferred whose callback is invoked when this command
3178 succeeds or whose errback is invoked if it fails.
3180 return self.sendCommand(Command('CHECK'))
3183 """Return the connection to the Authenticated state.
3185 This command is allowed in the Selected state.
3187 Issuing this command will also remove all messages flagged \\Deleted
3188 from the selected mailbox if it is opened in read-write mode,
3189 otherwise it indicates success by no messages are removed.
3192 @return: A deferred whose callback is invoked when the command
3193 completes successfully or whose errback is invoked if it fails.
3195 return self.sendCommand(Command('CLOSE'))
3199 """Return the connection to the Authenticate state.
3201 This command is allowed in the Selected state.
3203 Issuing this command will perform the same actions as issuing the
3204 close command, but will also generate an 'expunge' response for
3205 every message deleted.
3208 @return: A deferred whose callback is invoked with a list of the
3209 'expunge' responses when this command is successful or whose errback
3210 is invoked otherwise.
3214 d = self.sendCommand(Command(cmd, wantResponse=resp))
3215 d.addCallback(self.__cbExpunge)
3219 def __cbExpunge(self, (lines, last)):
3222 if len(parts) == 2 and parts[1] == 'EXPUNGE':
3223 ids.append(self._intOrRaise(parts[0], parts))
3227 def search(self, *queries, **kwarg):
3228 """Search messages in the currently selected mailbox
3230 This command is allowed in the Selected state.
3232 Any non-zero number of queries are accepted by this method, as
3233 returned by the C{Query}, C{Or}, and C{Not} functions.
3235 One keyword argument is accepted: if uid is passed in with a non-zero
3236 value, the server is asked to return message UIDs instead of message
3240 @return: A deferred whose callback will be invoked with a list of all
3241 the message sequence numbers return by the search, or whose errback
3242 will be invoked if there is an error.
3244 if kwarg.get('uid'):
3248 args = ' '.join(queries)
3249 d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,)))
3250 d.addCallback(self.__cbSearch)
3254 def __cbSearch(self, (lines, end)):
3257 if len(parts) > 0 and parts[0] == 'SEARCH':
3258 ids.extend([self._intOrRaise(p, parts) for p in parts[1:]])
3262 def fetchUID(self, messages, uid=0):
3263 """Retrieve the unique identifier for one or more messages
3265 This command is allowed in the Selected state.
3267 @type messages: C{MessageSet} or C{str}
3268 @param messages: A message sequence set
3271 @param uid: Indicates whether the message sequence set is of message
3272 numbers or of unique message IDs.
3275 @return: A deferred whose callback is invoked with a dict mapping
3276 message sequence numbers to unique message identifiers, or whose
3277 errback is invoked if there is an error.
3279 return self._fetch(messages, useUID=uid, uid=1)
3282 def fetchFlags(self, messages, uid=0):
3283 """Retrieve the flags for one or more messages
3285 This command is allowed in the Selected state.
3287 @type messages: C{MessageSet} or C{str}
3288 @param messages: The messages for which to retrieve flags.
3291 @param uid: Indicates whether the message sequence set is of message
3292 numbers or of unique message IDs.
3295 @return: A deferred whose callback is invoked with a dict mapping
3296 message numbers to lists of flags, or whose errback is invoked if
3299 return self._fetch(str(messages), useUID=uid, flags=1)
3302 def fetchInternalDate(self, messages, uid=0):
3303 """Retrieve the internal date associated with one or more messages
3305 This command is allowed in the Selected state.
3307 @type messages: C{MessageSet} or C{str}
3308 @param messages: The messages for which to retrieve the internal date.
3311 @param uid: Indicates whether the message sequence set is of message
3312 numbers or of unique message IDs.
3315 @return: A deferred whose callback is invoked with a dict mapping
3316 message numbers to date strings, or whose errback is invoked
3317 if there is an error. Date strings take the format of
3318 \"day-month-year time timezone\".
3320 return self._fetch(str(messages), useUID=uid, internaldate=1)
3323 def fetchEnvelope(self, messages, uid=0):
3324 """Retrieve the envelope data for one or more messages
3326 This command is allowed in the Selected state.
3328 @type messages: C{MessageSet} or C{str}
3329 @param messages: The messages for which to retrieve envelope data.
3332 @param uid: Indicates whether the message sequence set is of message
3333 numbers or of unique message IDs.
3336 @return: A deferred whose callback is invoked with a dict mapping
3337 message numbers to envelope data, or whose errback is invoked
3338 if there is an error. Envelope data consists of a sequence of the
3339 date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
3340 and message-id header fields. The date, subject, in-reply-to, and
3341 message-id fields are strings, while the from, sender, reply-to,
3342 to, cc, and bcc fields contain address data. Address data consists
3343 of a sequence of name, source route, mailbox name, and hostname.
3344 Fields which are not present for a particular address may be C{None}.
3346 return self._fetch(str(messages), useUID=uid, envelope=1)
3349 def fetchBodyStructure(self, messages, uid=0):
3350 """Retrieve the structure of the body of one or more messages
3352 This command is allowed in the Selected state.
3354 @type messages: C{MessageSet} or C{str}
3355 @param messages: The messages for which to retrieve body structure
3359 @param uid: Indicates whether the message sequence set is of message
3360 numbers or of unique message IDs.
3363 @return: A deferred whose callback is invoked with a dict mapping
3364 message numbers to body structure data, or whose errback is invoked
3365 if there is an error. Body structure data describes the MIME-IMB
3366 format of a message and consists of a sequence of mime type, mime
3367 subtype, parameters, content id, description, encoding, and size.
3368 The fields following the size field are variable: if the mime
3369 type/subtype is message/rfc822, the contained message's envelope
3370 information, body structure data, and number of lines of text; if
3371 the mime type is text, the number of lines of text. Extension fields
3372 may also be included; if present, they are: the MD5 hash of the body,
3373 body disposition, body language.
3375 return self._fetch(messages, useUID=uid, bodystructure=1)
3378 def fetchSimplifiedBody(self, messages, uid=0):
3379 """Retrieve the simplified body structure of one or more messages
3381 This command is allowed in the Selected state.
3383 @type messages: C{MessageSet} or C{str}
3384 @param messages: A message sequence set
3387 @param uid: Indicates whether the message sequence set is of message
3388 numbers or of unique message IDs.
3391 @return: A deferred whose callback is invoked with a dict mapping
3392 message numbers to body data, or whose errback is invoked
3393 if there is an error. The simplified body structure is the same
3394 as the body structure, except that extension fields will never be
3397 return self._fetch(messages, useUID=uid, body=1)
3400 def fetchMessage(self, messages, uid=0):
3401 """Retrieve one or more entire messages
3403 This command is allowed in the Selected state.
3405 @type messages: L{MessageSet} or C{str}
3406 @param messages: A message sequence set
3409 @param uid: Indicates whether the message sequence set is of message
3410 numbers or of unique message IDs.
3414 @return: A L{Deferred} which will fire with a C{dict} mapping message
3415 sequence numbers to C{dict}s giving message data for the
3416 corresponding message. If C{uid} is true, the inner dictionaries
3417 have a C{'UID'} key mapped to a C{str} giving the UID for the
3418 message. The text of the message is a C{str} associated with the
3419 C{'RFC822'} key in each dictionary.
3421 return self._fetch(messages, useUID=uid, rfc822=1)
3424 def fetchHeaders(self, messages, uid=0):
3425 """Retrieve headers of one or more messages
3427 This command is allowed in the Selected state.
3429 @type messages: C{MessageSet} or C{str}
3430 @param messages: A message sequence set
3433 @param uid: Indicates whether the message sequence set is of message
3434 numbers or of unique message IDs.
3437 @return: A deferred whose callback is invoked with a dict mapping
3438 message numbers to dicts of message headers, or whose errback is
3439 invoked if there is an error.
3441 return self._fetch(messages, useUID=uid, rfc822header=1)
3444 def fetchBody(self, messages, uid=0):
3445 """Retrieve body text of one or more messages
3447 This command is allowed in the Selected state.
3449 @type messages: C{MessageSet} or C{str}
3450 @param messages: A message sequence set
3453 @param uid: Indicates whether the message sequence set is of message
3454 numbers or of unique message IDs.
3457 @return: A deferred whose callback is invoked with a dict mapping
3458 message numbers to file-like objects containing body text, or whose
3459 errback is invoked if there is an error.
3461 return self._fetch(messages, useUID=uid, rfc822text=1)
3464 def fetchSize(self, messages, uid=0):
3465 """Retrieve the size, in octets, of one or more messages
3467 This command is allowed in the Selected state.
3469 @type messages: C{MessageSet} or C{str}
3470 @param messages: A message sequence set
3473 @param uid: Indicates whether the message sequence set is of message
3474 numbers or of unique message IDs.
3477 @return: A deferred whose callback is invoked with a dict mapping
3478 message numbers to sizes, or whose errback is invoked if there is
3481 return self._fetch(messages, useUID=uid, rfc822size=1)
3484 def fetchFull(self, messages, uid=0):
3485 """Retrieve several different fields of one or more messages
3487 This command is allowed in the Selected state. This is equivalent
3488 to issuing all of the C{fetchFlags}, C{fetchInternalDate},
3489 C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody}
3492 @type messages: C{MessageSet} or C{str}
3493 @param messages: A message sequence set
3496 @param uid: Indicates whether the message sequence set is of message
3497 numbers or of unique message IDs.
3500 @return: A deferred whose callback is invoked with a dict mapping
3501 message numbers to dict of the retrieved data values, or whose
3502 errback is invoked if there is an error. They dictionary keys
3503 are "flags", "date", "size", "envelope", and "body".
3506 messages, useUID=uid, flags=1, internaldate=1,
3507 rfc822size=1, envelope=1, body=1)
3510 def fetchAll(self, messages, uid=0):
3511 """Retrieve several different fields of one or more messages
3513 This command is allowed in the Selected state. This is equivalent
3514 to issuing all of the C{fetchFlags}, C{fetchInternalDate},
3515 C{fetchSize}, and C{fetchEnvelope} functions.
3517 @type messages: C{MessageSet} or C{str}
3518 @param messages: A message sequence set
3521 @param uid: Indicates whether the message sequence set is of message
3522 numbers or of unique message IDs.
3525 @return: A deferred whose callback is invoked with a dict mapping
3526 message numbers to dict of the retrieved data values, or whose
3527 errback is invoked if there is an error. They dictionary keys
3528 are "flags", "date", "size", and "envelope".
3531 messages, useUID=uid, flags=1, internaldate=1,
3532 rfc822size=1, envelope=1)
3535 def fetchFast(self, messages, uid=0):
3536 """Retrieve several different fields of one or more messages
3538 This command is allowed in the Selected state. This is equivalent
3539 to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and
3540 C{fetchSize} functions.
3542 @type messages: C{MessageSet} or C{str}
3543 @param messages: A message sequence set
3546 @param uid: Indicates whether the message sequence set is of message
3547 numbers or of unique message IDs.
3550 @return: A deferred whose callback is invoked with a dict mapping
3551 message numbers to dict of the retrieved data values, or whose
3552 errback is invoked if there is an error. They dictionary keys are
3553 "flags", "date", and "size".
3556 messages, useUID=uid, flags=1, internaldate=1, rfc822size=1)
3559 def _parseFetchPairs(self, fetchResponseList):
3561 Given the result of parsing a single I{FETCH} response, construct a
3562 C{dict} mapping response keys to response values.
3564 @param fetchResponseList: The result of parsing a I{FETCH} response
3565 with L{parseNestedParens} and extracting just the response data
3566 (that is, just the part that comes after C{"FETCH"}). The form
3567 of this input (and therefore the output of this method) is very
3568 disagreable. A valuable improvement would be to enumerate the
3569 possible keys (representing them as structured objects of some
3570 sort) rather than using strings and tuples of tuples of strings
3571 and so forth. This would allow the keys to be documented more
3572 easily and would allow for a much simpler application-facing API
3573 (one not based on looking up somewhat hard to predict keys in a
3574 dict). Since C{fetchResponseList} notionally represents a
3575 flattened sequence of pairs (identifying keys followed by their
3576 associated values), collapsing such complex elements of this
3577 list as C{["BODY", ["HEADER.FIELDS", ["SUBJECT"]]]} into a
3578 single object would also greatly simplify the implementation of
3581 @return: A C{dict} of the response data represented by C{pairs}. Keys
3582 in this dictionary are things like C{"RFC822.TEXT"}, C{"FLAGS"}, or
3583 C{("BODY", ("HEADER.FIELDS", ("SUBJECT",)))}. Values are entirely
3584 dependent on the key with which they are associated, but retain the
3585 same structured as produced by L{parseNestedParens}.
3588 responseParts = iter(fetchResponseList)
3591 key = responseParts.next()
3592 except StopIteration:
3596 value = responseParts.next()
3597 except StopIteration:
3598 raise IllegalServerResponse(
3599 "Not enough arguments", fetchResponseList)
3601 # The parsed forms of responses like:
3605 # BODY[HEADER.FIELDS (SUBJECT)] VALUE
3606 # BODY[HEADER.FIELDS (SUBJECT)]<N.M> VALUE
3610 # ["BODY", [], VALUE]
3611 # ["BODY", ["TEXT"], VALUE]
3612 # ["BODY", ["HEADER.FIELDS", ["SUBJECT"]], VALUE]
3613 # ["BODY", ["HEADER.FIELDS", ["SUBJECT"]], "<N.M>", VALUE]
3615 # Here, check for these cases and grab as many extra elements as
3616 # necessary to retrieve the body information.
3617 if key in ("BODY", "BODY.PEEK") and isinstance(value, list) and len(value) < 3:
3619 key = (key, tuple(value))
3621 key = (key, (value[0], tuple(value[1])))
3623 value = responseParts.next()
3624 except StopIteration:
3625 raise IllegalServerResponse(
3626 "Not enough arguments", fetchResponseList)
3628 # Handle partial ranges
3629 if value.startswith('<') and value.endswith('>'):
3633 # This isn't really a range, it's some content.
3636 key = key + (value,)
3638 value = responseParts.next()
3639 except StopIteration:
3640 raise IllegalServerResponse(
3641 "Not enough arguments", fetchResponseList)
3647 def _cbFetch(self, (lines, last), requestedParts, structured):
3650 if len(parts) == 3 and parts[1] == 'FETCH':
3651 id = self._intOrRaise(parts[0], parts)
3653 info[id] = [parts[2]]
3655 info[id][0].extend(parts[2])
3658 for (messageId, values) in info.iteritems():
3659 mapping = self._parseFetchPairs(values[0])
3660 results.setdefault(messageId, {}).update(mapping)
3663 for messageId in results.keys():
3664 values = results[messageId]
3665 for part in values.keys():
3666 if part not in requestedParts and part == 'FLAGS':
3667 flagChanges[messageId] = values['FLAGS']
3668 # Find flags in the result and get rid of them.
3669 for i in range(len(info[messageId][0])):
3670 if info[messageId][0][i] == 'FLAGS':
3671 del info[messageId][0][i:i+2]
3675 del results[messageId]
3678 self.flagsChanged(flagChanges)
3686 def fetchSpecific(self, messages, uid=0, headerType=None,
3687 headerNumber=None, headerArgs=None, peek=None,
3688 offset=None, length=None):
3689 """Retrieve a specific section of one or more messages
3691 @type messages: C{MessageSet} or C{str}
3692 @param messages: A message sequence set
3695 @param uid: Indicates whether the message sequence set is of message
3696 numbers or of unique message IDs.
3698 @type headerType: C{str}
3699 @param headerType: If specified, must be one of HEADER,
3700 HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, or TEXT, and will determine
3701 which part of the message is retrieved. For HEADER.FIELDS and
3702 HEADER.FIELDS.NOT, C{headerArgs} must be a sequence of header names.
3703 For MIME, C{headerNumber} must be specified.
3705 @type headerNumber: C{int} or C{int} sequence
3706 @param headerNumber: The nested rfc822 index specifying the
3707 entity to retrieve. For example, C{1} retrieves the first
3708 entity of the message, and C{(2, 1, 3}) retrieves the 3rd
3709 entity inside the first entity inside the second entity of
3712 @type headerArgs: A sequence of C{str}
3713 @param headerArgs: If C{headerType} is HEADER.FIELDS, these are the
3714 headers to retrieve. If it is HEADER.FIELDS.NOT, these are the
3715 headers to exclude from retrieval.
3718 @param peek: If true, cause the server to not set the \\Seen
3719 flag on this message as a result of this command.
3721 @type offset: C{int}
3722 @param offset: The number of octets at the beginning of the result
3725 @type length: C{int}
3726 @param length: The number of octets to retrieve.
3729 @return: A deferred whose callback is invoked with a mapping of
3730 message numbers to retrieved data, or whose errback is invoked
3731 if there is an error.
3733 fmt = '%s BODY%s[%s%s%s]%s'
3734 if headerNumber is None:
3736 elif isinstance(headerNumber, int):
3737 number = str(headerNumber)
3739 number = '.'.join(map(str, headerNumber))
3740 if headerType is None:
3743 header = '.' + headerType
3746 if header and headerType not in ('TEXT', 'MIME'):
3747 if headerArgs is not None:
3748 payload = ' (%s)' % ' '.join(headerArgs)
3756 extra = '<%d.%d>' % (offset, length)
3757 fetch = uid and 'UID FETCH' or 'FETCH'
3758 cmd = fmt % (messages, peek and '.PEEK' or '', number, header, payload, extra)
3759 d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
3760 d.addCallback(self._cbFetch, (), False)
3764 def _fetch(self, messages, useUID=0, **terms):
3765 fetch = useUID and 'UID FETCH' or 'FETCH'
3767 if 'rfc822text' in terms:
3768 del terms['rfc822text']
3769 terms['rfc822.text'] = True
3770 if 'rfc822size' in terms:
3771 del terms['rfc822size']
3772 terms['rfc822.size'] = True
3773 if 'rfc822header' in terms:
3774 del terms['rfc822header']
3775 terms['rfc822.header'] = True
3777 cmd = '%s (%s)' % (messages, ' '.join([s.upper() for s in terms.keys()]))
3778 d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
3779 d.addCallback(self._cbFetch, map(str.upper, terms.keys()), True)
3782 def setFlags(self, messages, flags, silent=1, uid=0):
3783 """Set the flags for one or more messages.
3785 This command is allowed in the Selected state.
3787 @type messages: C{MessageSet} or C{str}
3788 @param messages: A message sequence set
3790 @type flags: Any iterable of C{str}
3791 @param flags: The flags to set
3793 @type silent: C{bool}
3794 @param silent: If true, cause the server to supress its verbose
3798 @param uid: Indicates whether the message sequence set is of message
3799 numbers or of unique message IDs.
3802 @return: A deferred whose callback is invoked with a list of the
3803 the server's responses (C{[]} if C{silent} is true) or whose
3804 errback is invoked if there is an error.
3806 return self._store(str(messages), 'FLAGS', silent, flags, uid)
3808 def addFlags(self, messages, flags, silent=1, uid=0):
3809 """Add to the set flags for one or more messages.
3811 This command is allowed in the Selected state.
3813 @type messages: C{MessageSet} or C{str}
3814 @param messages: A message sequence set
3816 @type flags: Any iterable of C{str}
3817 @param flags: The flags to set
3819 @type silent: C{bool}
3820 @param silent: If true, cause the server to supress its verbose
3824 @param uid: Indicates whether the message sequence set is of message
3825 numbers or of unique message IDs.
3828 @return: A deferred whose callback is invoked with a list of the
3829 the server's responses (C{[]} if C{silent} is true) or whose
3830 errback is invoked if there is an error.
3832 return self._store(str(messages),'+FLAGS', silent, flags, uid)
3834 def removeFlags(self, messages, flags, silent=1, uid=0):
3835 """Remove from the set flags for one or more messages.
3837 This command is allowed in the Selected state.
3839 @type messages: C{MessageSet} or C{str}
3840 @param messages: A message sequence set
3842 @type flags: Any iterable of C{str}
3843 @param flags: The flags to set
3845 @type silent: C{bool}
3846 @param silent: If true, cause the server to supress its verbose
3850 @param uid: Indicates whether the message sequence set is of message
3851 numbers or of unique message IDs.
3854 @return: A deferred whose callback is invoked with a list of the
3855 the server's responses (C{[]} if C{silent} is true) or whose
3856 errback is invoked if there is an error.
3858 return self._store(str(messages), '-FLAGS', silent, flags, uid)
3861 def _store(self, messages, cmd, silent, flags, uid):
3863 cmd = cmd + '.SILENT'
3864 store = uid and 'UID STORE' or 'STORE'
3865 args = ' '.join((messages, cmd, '(%s)' % ' '.join(flags)))
3866 d = self.sendCommand(Command(store, args, wantResponse=('FETCH',)))
3869 expected = ('FLAGS',)
3870 d.addCallback(self._cbFetch, expected, True)
3874 def copy(self, messages, mailbox, uid):
3875 """Copy the specified messages to the specified mailbox.
3877 This command is allowed in the Selected state.
3879 @type messages: C{str}
3880 @param messages: A message sequence set
3882 @type mailbox: C{str}
3883 @param mailbox: The mailbox to which to copy the messages
3886 @param uid: If true, the C{messages} refers to message UIDs, rather
3887 than message sequence numbers.
3890 @return: A deferred whose callback is invoked with a true value
3891 when the copy is successful, or whose errback is invoked if there
3898 args = '%s %s' % (messages, _prepareMailboxName(mailbox))
3899 return self.sendCommand(Command(cmd, args))
3902 # IMailboxListener methods
3904 def modeChanged(self, writeable):
3907 def flagsChanged(self, newFlags):
3910 def newMessages(self, exists, recent):
3914 class IllegalIdentifierError(IMAP4Exception): pass
3916 def parseIdList(s, lastMessageId=None):
3918 Parse a message set search key into a C{MessageSet}.
3921 @param s: A string description of a id list, for example "1:3, 4:*"
3923 @type lastMessageId: C{int}
3924 @param lastMessageId: The last message sequence id or UID, depending on
3925 whether we are parsing the list in UID or sequence id context. The
3926 caller should pass in the correct value.
3928 @rtype: C{MessageSet}
3929 @return: A C{MessageSet} that contains the ids defined in the list
3932 parts = s.split(',')
3935 low, high = p.split(':', 1)
3945 if low is high is None:
3946 # *:* does not make sense
3947 raise IllegalIdentifierError(p)
3948 # non-positive values are illegal according to RFC 3501
3949 if ((low is not None and low <= 0) or
3950 (high is not None and high <= 0)):
3951 raise IllegalIdentifierError(p)
3952 # star means "highest value of an id in the mailbox"
3953 high = high or lastMessageId
3954 low = low or lastMessageId
3956 # RFC says that 2:4 and 4:2 are equivalent
3958 low, high = high, low
3959 res.extend((low, high))
3961 raise IllegalIdentifierError(p)
3968 if p is not None and p <= 0:
3969 raise IllegalIdentifierError(p)
3971 raise IllegalIdentifierError(p)
3973 res.extend(p or lastMessageId)
3976 class IllegalQueryError(IMAP4Exception): pass
3979 'ALL', 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'NEW', 'OLD', 'RECENT',
3980 'SEEN', 'UNANSWERED', 'UNDELETED', 'UNDRAFT', 'UNFLAGGED', 'UNSEEN'
3984 'LARGER', 'SMALLER', 'UID'
3987 def Query(sorted=0, **kwarg):
3988 """Create a query string
3990 Among the accepted keywords are::
3992 all : If set to a true value, search all messages in the
3995 answered : If set to a true value, search messages flagged with
3998 bcc : A substring to search the BCC header field for
4000 before : Search messages with an internal date before this
4001 value. The given date should be a string in the format
4002 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
4004 body : A substring to search the body of the messages for
4006 cc : A substring to search the CC header field for
4008 deleted : If set to a true value, search messages flagged with
4011 draft : If set to a true value, search messages flagged with
4014 flagged : If set to a true value, search messages flagged with
4017 from : A substring to search the From header field for
4019 header : A two-tuple of a header name and substring to search
4022 keyword : Search for messages with the given keyword set
4024 larger : Search for messages larger than this number of octets
4026 messages : Search only the given message sequence set.
4028 new : If set to a true value, search messages flagged with
4029 \\Recent but not \\Seen
4031 old : If set to a true value, search messages not flagged with
4034 on : Search messages with an internal date which is on this
4035 date. The given date should be a string in the format
4036 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
4038 recent : If set to a true value, search for messages flagged with
4041 seen : If set to a true value, search for messages flagged with
4044 sentbefore : Search for messages with an RFC822 'Date' header before
4045 this date. The given date should be a string in the format
4046 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
4048 senton : Search for messages with an RFC822 'Date' header which is
4049 on this date The given date should be a string in the format
4050 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
4052 sentsince : Search for messages with an RFC822 'Date' header which is
4053 after this date. The given date should be a string in the format
4054 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
4056 since : Search for messages with an internal date that is after
4057 this date.. The given date should be a string in the format
4058 of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
4060 smaller : Search for messages smaller than this number of octets
4062 subject : A substring to search the 'subject' header for
4064 text : A substring to search the entire message for
4066 to : A substring to search the 'to' header for
4068 uid : Search only the messages in the given message set
4070 unanswered : If set to a true value, search for messages not
4071 flagged with \\Answered
4073 undeleted : If set to a true value, search for messages not
4074 flagged with \\Deleted
4076 undraft : If set to a true value, search for messages not
4077 flagged with \\Draft
4079 unflagged : If set to a true value, search for messages not
4080 flagged with \\Flagged
4082 unkeyword : Search for messages without the given keyword set
4084 unseen : If set to a true value, search for messages not
4087 @type sorted: C{bool}
4088 @param sorted: If true, the output will be sorted, alphabetically.
4089 The standard does not require it, but it makes testing this function
4090 easier. The default is zero, and this should be acceptable for any
4094 @return: The formatted query string
4103 if k in _SIMPLE_BOOL and v:
4106 cmd.extend([k, v[0], '"%s"' % (v[1],)])
4107 elif k not in _NO_QUOTES:
4108 cmd.extend([k, '"%s"' % (v,)])
4110 cmd.extend([k, '%s' % (v,)])
4112 return '(%s)' % ' '.join(cmd)
4114 return ' '.join(cmd)
4117 """The disjunction of two or more queries"""
4119 raise IllegalQueryError, args
4120 elif len(args) == 2:
4121 return '(OR %s %s)' % args
4123 return '(OR %s %s)' % (args[0], Or(*args[1:]))
4126 """The negation of a query"""
4127 return '(NOT %s)' % (query,)
4129 class MismatchedNesting(IMAP4Exception):
4132 class MismatchedQuoting(IMAP4Exception):
4135 def wildcardToRegexp(wildcard, delim=None):
4136 wildcard = wildcard.replace('*', '(?:.*?)')
4138 wildcard = wildcard.replace('%', '(?:.*?)')
4140 wildcard = wildcard.replace('%', '(?:(?:[^%s])*?)' % re.escape(delim))
4141 return re.compile(wildcard, re.I)
4144 """Split a string into whitespace delimited tokens
4146 Tokens that would otherwise be separated but are surrounded by \"
4147 remain as a single token. Any token that is not quoted and is
4148 equal to \"NIL\" is tokenized as C{None}.
4151 @param s: The string to be split
4153 @rtype: C{list} of C{str}
4154 @return: A list of the resulting tokens
4156 @raise MismatchedQuoting: Raised if an odd number of quotes are present
4161 inQuote = inWord = False
4162 for i, c in enumerate(s):
4164 if i and s[i-1] == '\\':
4171 result.append(''.join(word))
4173 elif not inWord and not inQuote and c not in ('"' + string.whitespace):
4176 elif inWord and not inQuote and c in string.whitespace:
4184 elif inWord or inQuote:
4188 raise MismatchedQuoting(s)
4200 def splitOn(sequence, predicate, transformers):
4202 mode = predicate(sequence[0])
4204 for e in sequence[1:]:
4207 result.extend(transformers[mode](tmp))
4212 result.extend(transformers[mode](tmp))
4215 def collapseStrings(results):
4217 Turns a list of length-one strings and lists into a list of longer
4218 strings and lists. For example,
4220 ['a', 'b', ['c', 'd']] is returned as ['ab', ['cd']]
4222 @type results: C{list} of C{str} and C{list}
4223 @param results: The list to be collapsed
4225 @rtype: C{list} of C{str} and C{list}
4226 @return: A new list which is the collapsed form of C{results}
4230 listsList = [isinstance(s, types.ListType) for s in results]
4232 pred = lambda e: isinstance(e, types.TupleType)
4234 0: lambda e: splitQuoted(''.join(e)),
4235 1: lambda e: [''.join([i[0] for i in e])]
4237 for (i, c, isList) in zip(range(len(results)), results, listsList):
4239 if begun is not None:
4240 copy.extend(splitOn(results[begun:i], pred, tran))
4242 copy.append(collapseStrings(c))
4245 if begun is not None:
4246 copy.extend(splitOn(results[begun:], pred, tran))
4250 def parseNestedParens(s, handleLiteral = 1):
4251 """Parse an s-exp-like string into a more useful data structure.
4254 @param s: The s-exp-like string to parse
4256 @rtype: C{list} of C{str} and C{list}
4257 @return: A list containing the tokens present in the input.
4259 @raise MismatchedNesting: Raised if the number or placement
4260 of opening or closing parenthesis is invalid.
4272 contentStack[-1].append(s[i:i+2])
4276 inQuote = not inQuote
4277 contentStack[-1].append(c)
4281 contentStack[-1].append(c)
4282 inQuote = not inQuote
4284 elif handleLiteral and c == '{':
4285 end = s.find('}', i)
4287 raise ValueError, "Malformed literal"
4288 literalSize = int(s[i+1:end])
4289 contentStack[-1].append((s[end+3:end+3+literalSize],))
4290 i = end + 3 + literalSize
4291 elif c == '(' or c == '[':
4292 contentStack.append([])
4294 elif c == ')' or c == ']':
4295 contentStack[-2].append(contentStack.pop())
4298 contentStack[-1].append(c)
4301 raise MismatchedNesting(s)
4302 if len(contentStack) != 1:
4303 raise MismatchedNesting(s)
4304 return collapseStrings(contentStack[0])
4307 return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),)
4310 return '{%d}\r\n%s' % (len(s), s)
4313 def __init__(self, value):
4317 return str(self.value)
4319 _ATOM_SPECIALS = '(){ %*"'
4324 if c < '\x20' or c > '\x7f':
4326 if c in _ATOM_SPECIALS:
4330 def _prepareMailboxName(name):
4331 name = name.encode('imap4-utf-7')
4332 if _needsQuote(name):
4336 def _needsLiteral(s):
4337 # Change this to "return 1" to wig out stupid clients
4338 return '\n' in s or '\r' in s or len(s) > 1000
4340 def collapseNestedLists(items):
4341 """Turn a nested list structure into an s-exp-like string.
4343 Strings in C{items} will be sent as literals if they contain CR or LF,
4344 otherwise they will be quoted. References to None in C{items} will be
4345 translated to the atom NIL. Objects with a 'read' attribute will have
4346 it called on them with no arguments and the returned string will be
4347 inserted into the output as a literal. Integers will be converted to
4348 strings and inserted into the output unquoted. Instances of
4349 C{DontQuoteMe} will be converted to strings and inserted into the output
4352 This function used to be much nicer, and only quote things that really
4353 needed to be quoted (and C{DontQuoteMe} did not exist), however, many
4354 broken IMAP4 clients were unable to deal with this level of sophistication,
4355 forcing the current behavior to be adopted for practical reasons.
4357 @type items: Any iterable
4364 pieces.extend([' ', 'NIL'])
4365 elif isinstance(i, (DontQuoteMe, int, long)):
4366 pieces.extend([' ', str(i)])
4367 elif isinstance(i, types.StringTypes):
4368 if _needsLiteral(i):
4369 pieces.extend([' ', '{', str(len(i)), '}', IMAP4Server.delimiter, i])
4371 pieces.extend([' ', _quote(i)])
4372 elif hasattr(i, 'read'):
4374 pieces.extend([' ', '{', str(len(d)), '}', IMAP4Server.delimiter, d])
4376 pieces.extend([' ', '(%s)' % (collapseNestedLists(i),)])
4377 return ''.join(pieces[1:])
4380 class IClientAuthentication(Interface):
4382 """Return an identifier associated with this authentication scheme.
4387 def challengeResponse(secret, challenge):
4388 """Generate a challenge response string"""
4392 class CramMD5ClientAuthenticator:
4393 implements(IClientAuthentication)
4395 def __init__(self, user):
4401 def challengeResponse(self, secret, chal):
4402 response = hmac.HMAC(secret, chal).hexdigest()
4403 return '%s %s' % (self.user, response)
4407 class LOGINAuthenticator:
4408 implements(IClientAuthentication)
4410 def __init__(self, user):
4412 self.challengeResponse = self.challengeUsername
4417 def challengeUsername(self, secret, chal):
4418 # Respond to something like "Username:"
4419 self.challengeResponse = self.challengeSecret
4422 def challengeSecret(self, secret, chal):
4423 # Respond to something like "Password:"
4426 class PLAINAuthenticator:
4427 implements(IClientAuthentication)
4429 def __init__(self, user):
4435 def challengeResponse(self, secret, chal):
4436 return '\0%s\0%s' % (self.user, secret)
4439 class MailboxException(IMAP4Exception): pass
4441 class MailboxCollision(MailboxException):
4443 return 'Mailbox named %s already exists' % self.args
4445 class NoSuchMailbox(MailboxException):
4447 return 'No mailbox named %s exists' % self.args
4449 class ReadOnlyMailbox(MailboxException):
4451 return 'Mailbox open in read-only state'
4454 class IAccount(Interface):
4455 """Interface for Account classes
4457 Implementors of this interface should consider implementing
4458 C{INamespacePresenter}.
4461 def addMailbox(name, mbox = None):
4462 """Add a new mailbox to this account
4465 @param name: The name associated with this mailbox. It may not
4466 contain multiple hierarchical parts.
4468 @type mbox: An object implementing C{IMailbox}
4469 @param mbox: The mailbox to associate with this name. If C{None},
4470 a suitable default is created and used.
4472 @rtype: C{Deferred} or C{bool}
4473 @return: A true value if the creation succeeds, or a deferred whose
4474 callback will be invoked when the creation succeeds.
4476 @raise MailboxException: Raised if this mailbox cannot be added for
4477 some reason. This may also be raised asynchronously, if a C{Deferred}
4481 def create(pathspec):
4482 """Create a new mailbox from the given hierarchical name.
4484 @type pathspec: C{str}
4485 @param pathspec: The full hierarchical name of a new mailbox to create.
4486 If any of the inferior hierarchical names to this one do not exist,
4487 they are created as well.
4489 @rtype: C{Deferred} or C{bool}
4490 @return: A true value if the creation succeeds, or a deferred whose
4491 callback will be invoked when the creation succeeds.
4493 @raise MailboxException: Raised if this mailbox cannot be added.
4494 This may also be raised asynchronously, if a C{Deferred} is
4498 def select(name, rw=True):
4499 """Acquire a mailbox, given its name.
4502 @param name: The mailbox to acquire
4505 @param rw: If a true value, request a read-write version of this
4506 mailbox. If a false value, request a read-only version.
4508 @rtype: Any object implementing C{IMailbox} or C{Deferred}
4509 @return: The mailbox object, or a C{Deferred} whose callback will
4510 be invoked with the mailbox object. None may be returned if the
4511 specified mailbox may not be selected for any reason.
4515 """Delete the mailbox with the specified name.
4518 @param name: The mailbox to delete.
4520 @rtype: C{Deferred} or C{bool}
4521 @return: A true value if the mailbox is successfully deleted, or a
4522 C{Deferred} whose callback will be invoked when the deletion
4525 @raise MailboxException: Raised if this mailbox cannot be deleted.
4526 This may also be raised asynchronously, if a C{Deferred} is returned.
4529 def rename(oldname, newname):
4532 @type oldname: C{str}
4533 @param oldname: The current name of the mailbox to rename.
4535 @type newname: C{str}
4536 @param newname: The new name to associate with the mailbox.
4538 @rtype: C{Deferred} or C{bool}
4539 @return: A true value if the mailbox is successfully renamed, or a
4540 C{Deferred} whose callback will be invoked when the rename operation
4543 @raise MailboxException: Raised if this mailbox cannot be
4544 renamed. This may also be raised asynchronously, if a C{Deferred}
4548 def isSubscribed(name):
4549 """Check the subscription status of a mailbox
4552 @param name: The name of the mailbox to check
4554 @rtype: C{Deferred} or C{bool}
4555 @return: A true value if the given mailbox is currently subscribed
4556 to, a false value otherwise. A C{Deferred} may also be returned
4557 whose callback will be invoked with one of these values.
4560 def subscribe(name):
4561 """Subscribe to a mailbox
4564 @param name: The name of the mailbox to subscribe to
4566 @rtype: C{Deferred} or C{bool}
4567 @return: A true value if the mailbox is subscribed to successfully,
4568 or a Deferred whose callback will be invoked with this value when
4569 the subscription is successful.
4571 @raise MailboxException: Raised if this mailbox cannot be
4572 subscribed to. This may also be raised asynchronously, if a
4573 C{Deferred} is returned.
4576 def unsubscribe(name):
4577 """Unsubscribe from a mailbox
4580 @param name: The name of the mailbox to unsubscribe from
4582 @rtype: C{Deferred} or C{bool}
4583 @return: A true value if the mailbox is unsubscribed from successfully,
4584 or a Deferred whose callback will be invoked with this value when
4585 the unsubscription is successful.
4587 @raise MailboxException: Raised if this mailbox cannot be
4588 unsubscribed from. This may also be raised asynchronously, if a
4589 C{Deferred} is returned.
4592 def listMailboxes(ref, wildcard):
4593 """List all the mailboxes that meet a certain criteria
4596 @param ref: The context in which to apply the wildcard
4598 @type wildcard: C{str}
4599 @param wildcard: An expression against which to match mailbox names.
4600 '*' matches any number of characters in a mailbox name, and '%'
4601 matches similarly, but will not match across hierarchical boundaries.
4603 @rtype: C{list} of C{tuple}
4604 @return: A list of C{(mailboxName, mailboxObject)} which meet the
4605 given criteria. C{mailboxObject} should implement either
4606 C{IMailboxInfo} or C{IMailbox}. A Deferred may also be returned.
4609 class INamespacePresenter(Interface):
4610 def getPersonalNamespaces():
4611 """Report the available personal namespaces.
4613 Typically there should be only one personal namespace. A common
4614 name for it is \"\", and its hierarchical delimiter is usually
4617 @rtype: iterable of two-tuples of strings
4618 @return: The personal namespaces and their hierarchical delimiters.
4619 If no namespaces of this type exist, None should be returned.
4622 def getSharedNamespaces():
4623 """Report the available shared namespaces.
4625 Shared namespaces do not belong to any individual user but are
4626 usually to one or more of them. Examples of shared namespaces
4627 might be \"#news\" for a usenet gateway.
4629 @rtype: iterable of two-tuples of strings
4630 @return: The shared namespaces and their hierarchical delimiters.
4631 If no namespaces of this type exist, None should be returned.
4634 def getUserNamespaces():
4635 """Report the available user namespaces.
4637 These are namespaces that contain folders belonging to other users
4638 access to which this account has been granted.
4640 @rtype: iterable of two-tuples of strings
4641 @return: The user namespaces and their hierarchical delimiters.
4642 If no namespaces of this type exist, None should be returned.
4646 class MemoryAccount(object):
4647 implements(IAccount, INamespacePresenter)
4650 subscriptions = None
4653 def __init__(self, name):
4656 self.subscriptions = []
4658 def allocateID(self):
4666 def addMailbox(self, name, mbox = None):
4668 if self.mailboxes.has_key(name):
4669 raise MailboxCollision, name
4671 mbox = self._emptyMailbox(name, self.allocateID())
4672 self.mailboxes[name] = mbox
4675 def create(self, pathspec):
4676 paths = filter(None, pathspec.split('/'))
4677 for accum in range(1, len(paths)):
4679 self.addMailbox('/'.join(paths[:accum]))
4680 except MailboxCollision:
4683 self.addMailbox('/'.join(paths))
4684 except MailboxCollision:
4685 if not pathspec.endswith('/'):
4689 def _emptyMailbox(self, name, id):
4690 raise NotImplementedError
4692 def select(self, name, readwrite=1):
4693 return self.mailboxes.get(name.upper())
4695 def delete(self, name):
4697 # See if this mailbox exists at all
4698 mbox = self.mailboxes.get(name)
4700 raise MailboxException("No such mailbox")
4701 # See if this box is flagged \Noselect
4702 if r'\Noselect' in mbox.getFlags():
4703 # Check for hierarchically inferior mailboxes with this one
4704 # as part of their root.
4705 for others in self.mailboxes.keys():
4706 if others != name and others.startswith(name):
4707 raise MailboxException, "Hierarchically inferior mailboxes exist and \\Noselect is set"
4710 # iff there are no hierarchically inferior names, we will
4711 # delete it from our ken.
4712 if self._inferiorNames(name) > 1:
4713 del self.mailboxes[name]
4715 def rename(self, oldname, newname):
4716 oldname = oldname.upper()
4717 newname = newname.upper()
4718 if not self.mailboxes.has_key(oldname):
4719 raise NoSuchMailbox, oldname
4721 inferiors = self._inferiorNames(oldname)
4722 inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
4724 for (old, new) in inferiors:
4725 if self.mailboxes.has_key(new):
4726 raise MailboxCollision, new
4728 for (old, new) in inferiors:
4729 self.mailboxes[new] = self.mailboxes[old]
4730 del self.mailboxes[old]
4732 def _inferiorNames(self, name):
4734 for infname in self.mailboxes.keys():
4735 if infname.startswith(name):
4736 inferiors.append(infname)
4739 def isSubscribed(self, name):
4740 return name.upper() in self.subscriptions
4742 def subscribe(self, name):
4744 if name not in self.subscriptions:
4745 self.subscriptions.append(name)
4747 def unsubscribe(self, name):
4749 if name not in self.subscriptions:
4750 raise MailboxException, "Not currently subscribed to " + name
4751 self.subscriptions.remove(name)
4753 def listMailboxes(self, ref, wildcard):
4754 ref = self._inferiorNames(ref.upper())
4755 wildcard = wildcardToRegexp(wildcard, '/')
4756 return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]
4759 ## INamespacePresenter
4761 def getPersonalNamespaces(self):
4764 def getSharedNamespaces(self):
4767 def getOtherNamespaces(self):
4772 _statusRequestDict = {
4773 'MESSAGES': 'getMessageCount',
4774 'RECENT': 'getRecentCount',
4775 'UIDNEXT': 'getUIDNext',
4776 'UIDVALIDITY': 'getUIDValidity',
4777 'UNSEEN': 'getUnseenCount'
4779 def statusRequestHelper(mbox, names):
4782 r[n] = getattr(mbox, _statusRequestDict[n.upper()])()
4785 def parseAddr(addr):
4787 return [(None, None, None),]
4788 addrs = email.Utils.getaddresses([addr])
4789 return [[fn or None, None] + addr.split('@') for fn, addr in addrs]
4791 def getEnvelope(msg):
4792 headers = msg.getHeaders(True)
4793 date = headers.get('date')
4794 subject = headers.get('subject')
4795 from_ = headers.get('from')
4796 sender = headers.get('sender', from_)
4797 reply_to = headers.get('reply-to', from_)
4798 to = headers.get('to')
4799 cc = headers.get('cc')
4800 bcc = headers.get('bcc')
4801 in_reply_to = headers.get('in-reply-to')
4802 mid = headers.get('message-id')
4803 return (date, subject, parseAddr(from_), parseAddr(sender),
4804 reply_to and parseAddr(reply_to), to and parseAddr(to),
4805 cc and parseAddr(cc), bcc and parseAddr(bcc), in_reply_to, mid)
4807 def getLineCount(msg):
4808 # XXX - Super expensive, CACHE THIS VALUE FOR LATER RE-USE
4809 # XXX - This must be the number of lines in the ENCODED version
4811 for _ in msg.getBodyFile():
4816 if s[0] == s[-1] == '"':
4820 def getBodyStructure(msg, extended=False):
4821 # XXX - This does not properly handle multipart messages
4822 # BODYSTRUCTURE is obscenely complex and criminally under-documented.
4825 headers = 'content-type', 'content-id', 'content-description', 'content-transfer-encoding'
4826 headers = msg.getHeaders(False, *headers)
4827 mm = headers.get('content-type')
4829 mm = ''.join(mm.splitlines())
4830 mimetype = mm.split(';')
4832 type = mimetype[0].split('/', 1)
4836 elif len(type) == 2:
4839 major = minor = None
4840 attrs = dict([x.strip().lower().split('=', 1) for x in mimetype[1:]])
4842 major = minor = None
4844 major = minor = None
4847 size = str(msg.getSize())
4848 unquotedAttrs = [(k, unquote(v)) for (k, v) in attrs.iteritems()]
4850 major, minor, # Main and Sub MIME types
4851 unquotedAttrs, # content-type parameter list
4852 headers.get('content-id'),
4853 headers.get('content-description'),
4854 headers.get('content-transfer-encoding'),
4855 size, # Number of octets total
4858 if major is not None:
4859 if major.lower() == 'text':
4860 result.append(str(getLineCount(msg)))
4861 elif (major.lower(), minor.lower()) == ('message', 'rfc822'):
4862 contained = msg.getSubPart(0)
4863 result.append(getEnvelope(contained))
4864 result.append(getBodyStructure(contained, False))
4865 result.append(str(getLineCount(contained)))
4867 if not extended or major is None:
4870 if major.lower() != 'multipart':
4871 headers = 'content-md5', 'content-disposition', 'content-language'
4872 headers = msg.getHeaders(False, *headers)
4873 disp = headers.get('content-disposition')
4875 # XXX - I dunno if this is really right
4877 disp = disp.split('; ')
4879 disp = (disp[0].lower(), None)
4881 disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
4883 result.append(headers.get('content-md5'))
4885 result.append(headers.get('content-language'))
4891 submsg = msg.getSubPart(i)
4892 result.append(getBodyStructure(submsg))
4895 result.append(minor)
4896 result.append(attrs.items())
4898 # XXX - I dunno if this is really right
4899 headers = msg.getHeaders(False, 'content-disposition', 'content-language')
4900 disp = headers.get('content-disposition')
4902 disp = disp.split('; ')
4904 disp = (disp[0].lower(), None)
4906 disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
4909 result.append(headers.get('content-language'))
4913 class IMessagePart(Interface):
4914 def getHeaders(negate, *names):
4915 """Retrieve a group of message headers.
4917 @type names: C{tuple} of C{str}
4918 @param names: The names of the headers to retrieve or omit.
4920 @type negate: C{bool}
4921 @param negate: If True, indicates that the headers listed in C{names}
4922 should be omitted from the return value, rather than included.
4925 @return: A mapping of header field names to header field values
4929 """Retrieve a file object containing only the body of this message.
4933 """Retrieve the total size, in octets, of this message.
4939 """Indicate whether this message has subparts.
4944 def getSubPart(part):
4945 """Retrieve a MIME sub-message
4948 @param part: The number of the part to retrieve, indexed from 0.
4950 @raise IndexError: Raised if the specified part does not exist.
4951 @raise TypeError: Raised if this message is not multipart.
4953 @rtype: Any object implementing C{IMessagePart}.
4954 @return: The specified sub-part.
4957 class IMessage(IMessagePart):
4959 """Retrieve the unique identifier associated with this message.
4963 """Retrieve the flags associated with this message.
4966 @return: The flags, represented as strings.
4969 def getInternalDate():
4970 """Retrieve the date internally associated with this message.
4973 @return: An RFC822-formatted date string.
4976 class IMessageFile(Interface):
4977 """Optional message interface for representing messages as files.
4979 If provided by message objects, this interface will be used instead
4980 the more complex MIME-based interface.
4983 """Return an file-like object opened for reading.
4985 Reading from the returned file will return all the bytes
4986 of which this message consists.
4989 class ISearchableMailbox(Interface):
4990 def search(query, uid):
4991 """Search for messages that meet the given query criteria.
4993 If this interface is not implemented by the mailbox, L{IMailbox.fetch}
4994 and various methods of L{IMessage} will be used instead.
4996 Implementations which wish to offer better performance than the
4997 default implementation should implement this interface.
4999 @type query: C{list}
5000 @param query: The search criteria
5003 @param uid: If true, the IDs specified in the query are UIDs;
5004 otherwise they are message sequence IDs.
5006 @rtype: C{list} or C{Deferred}
5007 @return: A list of message sequence numbers or message UIDs which
5008 match the search criteria or a C{Deferred} whose callback will be
5009 invoked with such a list.
5012 class IMessageCopier(Interface):
5013 def copy(messageObject):
5014 """Copy the given message object into this mailbox.
5016 The message object will be one which was previously returned by
5019 Implementations which wish to offer better performance than the
5020 default implementation should implement this interface.
5022 If this interface is not implemented by the mailbox, IMailbox.addMessage
5023 will be used instead.
5025 @rtype: C{Deferred} or C{int}
5026 @return: Either the UID of the message or a Deferred which fires
5027 with the UID when the copy finishes.
5030 class IMailboxInfo(Interface):
5031 """Interface specifying only the methods required for C{listMailboxes}.
5033 Implementations can return objects implementing only these methods for
5034 return to C{listMailboxes} if it can allow them to operate more
5039 """Return the flags defined in this mailbox
5041 Flags with the \\ prefix are reserved for use as system flags.
5043 @rtype: C{list} of C{str}
5044 @return: A list of the flags that can be set on messages in this mailbox.
5047 def getHierarchicalDelimiter():
5048 """Get the character which delimits namespaces for in this mailbox.
5053 class IMailbox(IMailboxInfo):
5054 def getUIDValidity():
5055 """Return the unique validity identifier for this mailbox.
5061 """Return the likely UID for the next message added to this mailbox.
5066 def getUID(message):
5067 """Return the UID of a message in the mailbox
5069 @type message: C{int}
5070 @param message: The message sequence number
5073 @return: The UID of the message.
5076 def getMessageCount():
5077 """Return the number of messages in this mailbox.
5082 def getRecentCount():
5083 """Return the number of messages with the 'Recent' flag.
5088 def getUnseenCount():
5089 """Return the number of messages with the 'Unseen' flag.
5095 """Get the read/write status of the mailbox.
5098 @return: A true value if write permission is allowed, a false value otherwise.
5102 """Called before this mailbox is deleted, permanently.
5104 If necessary, all resources held by this mailbox should be cleaned
5105 up here. This function _must_ set the \\Noselect flag on this
5109 def requestStatus(names):
5110 """Return status information about this mailbox.
5112 Mailboxes which do not intend to do any special processing to
5113 generate the return value, C{statusRequestHelper} can be used
5114 to build the dictionary by calling the other interface methods
5115 which return the data for each name.
5117 @type names: Any iterable
5118 @param names: The status names to return information regarding.
5119 The possible values for each name are: MESSAGES, RECENT, UIDNEXT,
5120 UIDVALIDITY, UNSEEN.
5122 @rtype: C{dict} or C{Deferred}
5123 @return: A dictionary containing status information about the
5124 requested names is returned. If the process of looking this
5125 information up would be costly, a deferred whose callback will
5126 eventually be passed this dictionary is returned instead.
5129 def addListener(listener):
5130 """Add a mailbox change listener
5132 @type listener: Any object which implements C{IMailboxListener}
5133 @param listener: An object to add to the set of those which will
5134 be notified when the contents of this mailbox change.
5137 def removeListener(listener):
5138 """Remove a mailbox change listener
5140 @type listener: Any object previously added to and not removed from
5141 this mailbox as a listener.
5142 @param listener: The object to remove from the set of listeners.
5144 @raise ValueError: Raised when the given object is not a listener for
5148 def addMessage(message, flags = (), date = None):
5149 """Add the given message to this mailbox.
5151 @type message: A file-like object
5152 @param message: The RFC822 formatted message
5154 @type flags: Any iterable of C{str}
5155 @param flags: The flags to associate with this message
5158 @param date: If specified, the date to associate with this
5162 @return: A deferred whose callback is invoked with the message
5163 id if the message is added successfully and whose errback is
5166 @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
5171 """Remove all messages flagged \\Deleted.
5173 @rtype: C{list} or C{Deferred}
5174 @return: The list of message sequence numbers which were deleted,
5175 or a C{Deferred} whose callback will be invoked with such a list.
5177 @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
5181 def fetch(messages, uid):
5182 """Retrieve one or more messages.
5184 @type messages: C{MessageSet}
5185 @param messages: The identifiers of messages to retrieve information
5189 @param uid: If true, the IDs specified in the query are UIDs;
5190 otherwise they are message sequence IDs.
5192 @rtype: Any iterable of two-tuples of message sequence numbers and
5193 implementors of C{IMessage}.
5196 def store(messages, flags, mode, uid):
5197 """Set the flags of one or more messages.
5199 @type messages: A MessageSet object with the list of messages requested
5200 @param messages: The identifiers of the messages to set the flags of.
5202 @type flags: sequence of C{str}
5203 @param flags: The flags to set, unset, or add.
5205 @type mode: -1, 0, or 1
5206 @param mode: If mode is -1, these flags should be removed from the
5207 specified messages. If mode is 1, these flags should be added to
5208 the specified messages. If mode is 0, all existing flags should be
5209 cleared and these flags should be added.
5212 @param uid: If true, the IDs specified in the query are UIDs;
5213 otherwise they are message sequence IDs.
5215 @rtype: C{dict} or C{Deferred}
5216 @return: A C{dict} mapping message sequence numbers to sequences of C{str}
5217 representing the flags set on the message after this operation has
5218 been performed, or a C{Deferred} whose callback will be invoked with
5221 @raise ReadOnlyMailbox: Raised if this mailbox is not open for
5225 class ICloseableMailbox(Interface):
5226 """A supplementary interface for mailboxes which require cleanup on close.
5228 Implementing this interface is optional. If it is implemented, the protocol
5229 code will call the close method defined whenever a mailbox is closed.
5232 """Close this mailbox.
5234 @return: A C{Deferred} which fires when this mailbox
5235 has been closed, or None if the mailbox can be closed
5239 def _formatHeaders(headers):
5240 hdrs = [': '.join((k.title(), '\r\n'.join(v.splitlines()))) for (k, v)
5241 in headers.iteritems()]
5242 hdrs = '\r\n'.join(hdrs) + '\r\n'
5249 yield m.getSubPart(i)
5254 def iterateInReactor(i):
5255 """Consume an interator at most a single iteration per reactor iteration.
5257 If the iterator produces a Deferred, the next iteration will not occur
5258 until the Deferred fires, otherwise the next iteration will be taken
5259 in the next reactor iteration.
5262 @return: A deferred which fires (with None) when the iterator is
5263 exhausted or whose errback is called if there is an exception.
5265 from twisted.internet import reactor
5266 d = defer.Deferred()
5270 except StopIteration:
5275 if isinstance(r, defer.Deferred):
5278 reactor.callLater(0, go, r)
5282 class MessageProducer:
5283 CHUNK_SIZE = 2 ** 2 ** 2 ** 2
5285 def __init__(self, msg, buffer = None, scheduler = None):
5286 """Produce this message.
5288 @param msg: The message I am to produce.
5289 @type msg: L{IMessage}
5291 @param buffer: A buffer to hold the message in. If None, I will
5292 use a L{tempfile.TemporaryFile}.
5293 @type buffer: file-like
5297 buffer = tempfile.TemporaryFile()
5298 self.buffer = buffer
5299 if scheduler is None:
5300 scheduler = iterateInReactor
5301 self.scheduler = scheduler
5302 self.write = self.buffer.write
5304 def beginProducing(self, consumer):
5305 self.consumer = consumer
5306 return self.scheduler(self._produce())
5309 headers = self.msg.getHeaders(True)
5311 if self.msg.isMultipart():
5312 content = headers.get('content-type')
5313 parts = [x.split('=', 1) for x in content.split(';')[1:]]
5314 parts = dict([(k.lower().strip(), v) for (k, v) in parts])
5315 boundary = parts.get('boundary')
5316 if boundary is None:
5318 boundary = '----=_%f_boundary_%f' % (time.time(), random.random())
5319 headers['content-type'] += '; boundary="%s"' % (boundary,)
5321 if boundary.startswith('"') and boundary.endswith('"'):
5322 boundary = boundary[1:-1]
5324 self.write(_formatHeaders(headers))
5326 if self.msg.isMultipart():
5327 for p in subparts(self.msg):
5328 self.write('\r\n--%s\r\n' % (boundary,))
5329 yield MessageProducer(p, self.buffer, self.scheduler
5330 ).beginProducing(None
5332 self.write('\r\n--%s--\r\n' % (boundary,))
5334 f = self.msg.getBodyFile()
5336 b = f.read(self.CHUNK_SIZE)
5338 self.buffer.write(b)
5343 self.buffer.seek(0, 0)
5344 yield FileProducer(self.buffer
5345 ).beginProducing(self.consumer
5346 ).addCallback(lambda _: self
5351 # Response should be a list of fields from the message:
5352 # date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
5355 # from, sender, reply-to, to, cc, and bcc are themselves lists of
5356 # address information:
5357 # personal name, source route, mailbox name, host name
5359 # reply-to and sender must not be None. If not present in a message
5360 # they should be defaulted to the value of the from field.
5362 __str__ = lambda self: 'envelope'
5366 __str__ = lambda self: 'flags'
5369 type = 'internaldate'
5370 __str__ = lambda self: 'internaldate'
5373 type = 'rfc822header'
5374 __str__ = lambda self: 'rfc822.header'
5378 __str__ = lambda self: 'rfc822.text'
5382 __str__ = lambda self: 'rfc822.size'
5386 __str__ = lambda self: 'rfc822'
5390 __str__ = lambda self: 'uid'
5401 partialLength = None
5407 part = '.'.join([str(x + 1) for x in self.part])
5412 base += '[%s%s%s]' % (part, separator, self.header,)
5414 base += '[%s%sTEXT]' % (part, separator)
5416 base += '[%s%sMIME]' % (part, separator)
5418 base += '[%s]' % (part,)
5419 if self.partialBegin is not None:
5420 base += '<%d.%d>' % (self.partialBegin, self.partialLength)
5423 class BodyStructure:
5424 type = 'bodystructure'
5425 __str__ = lambda self: 'bodystructure'
5427 # These three aren't top-level, they don't need type indicators
5439 for f in self.fields:
5444 base += ' (%s)' % ' '.join(fields)
5446 base = '.'.join([str(x + 1) for x in self.part]) + '.' + base
5457 _simple_fetch_att = [
5458 ('envelope', Envelope),
5460 ('internaldate', InternalDate),
5461 ('rfc822.header', RFC822Header),
5462 ('rfc822.text', RFC822Text),
5463 ('rfc822.size', RFC822Size),
5466 ('bodystructure', BodyStructure),
5470 self.state = ['initial']
5474 def parseString(self, s):
5475 s = self.remaining + s
5477 while s or self.state:
5478 # print 'Entering state_' + self.state[-1] + ' with', repr(s)
5479 state = self.state.pop()
5481 used = getattr(self, 'state_' + state)(s)
5483 self.state.append(state)
5486 # print state, 'consumed', repr(s[:used])
5491 def state_initial(self, s):
5492 # In the initial state, the literals "ALL", "FULL", and "FAST"
5493 # are accepted, as is a ( indicating the beginning of a fetch_att
5494 # token, as is the beginning of a fetch_att token.
5499 if l.startswith('all'):
5500 self.result.extend((
5501 self.Flags(), self.InternalDate(),
5502 self.RFC822Size(), self.Envelope()
5505 if l.startswith('full'):
5506 self.result.extend((
5507 self.Flags(), self.InternalDate(),
5508 self.RFC822Size(), self.Envelope(),
5512 if l.startswith('fast'):
5513 self.result.extend((
5514 self.Flags(), self.InternalDate(), self.RFC822Size(),
5518 if l.startswith('('):
5519 self.state.extend(('close_paren', 'maybe_fetch_att', 'fetch_att'))
5522 self.state.append('fetch_att')
5525 def state_close_paren(self, s):
5526 if s.startswith(')'):
5528 raise Exception("Missing )")
5530 def state_whitespace(self, s):
5531 # Eat up all the leading whitespace
5532 if not s or not s[0].isspace():
5533 raise Exception("Whitespace expected, none found")
5535 for i in range(len(s)):
5536 if not s[i].isspace():
5540 def state_maybe_fetch_att(self, s):
5541 if not s.startswith(')'):
5542 self.state.extend(('maybe_fetch_att', 'fetch_att', 'whitespace'))
5545 def state_fetch_att(self, s):
5546 # Allowed fetch_att tokens are "ENVELOPE", "FLAGS", "INTERNALDATE",
5547 # "RFC822", "RFC822.HEADER", "RFC822.SIZE", "RFC822.TEXT", "BODY",
5548 # "BODYSTRUCTURE", "UID",
5549 # "BODY [".PEEK"] [<section>] ["<" <number> "." <nz_number> ">"]
5552 for (name, cls) in self._simple_fetch_att:
5553 if l.startswith(name):
5554 self.result.append(cls())
5558 if l.startswith('body.peek'):
5561 elif l.startswith('body'):
5564 raise Exception("Nothing recognized in fetch_att: %s" % (l,))
5566 self.pending_body = b
5567 self.state.extend(('got_body', 'maybe_partial', 'maybe_section'))
5570 def state_got_body(self, s):
5571 self.result.append(self.pending_body)
5572 del self.pending_body
5575 def state_maybe_section(self, s):
5576 if not s.startswith("["):
5579 self.state.extend(('section', 'part_number'))
5582 _partExpr = re.compile(r'(\d+(?:\.\d+)*)\.?')
5583 def state_part_number(self, s):
5584 m = self._partExpr.match(s)
5586 self.parts = [int(p) - 1 for p in m.groups()[0].split('.')]
5592 def state_section(self, s):
5593 # Grab "HEADER]" or "HEADER.FIELDS (Header list)]" or
5594 # "HEADER.FIELDS.NOT (Header list)]" or "TEXT]" or "MIME]" or
5599 if l.startswith(']'):
5600 self.pending_body.empty = True
5602 elif l.startswith('header]'):
5603 h = self.pending_body.header = self.Header()
5607 elif l.startswith('text]'):
5608 self.pending_body.text = self.Text()
5610 elif l.startswith('mime]'):
5611 self.pending_body.mime = self.MIME()
5615 if l.startswith('header.fields.not'):
5618 elif l.startswith('header.fields'):
5621 raise Exception("Unhandled section contents: %r" % (l,))
5623 self.pending_body.header = h
5624 self.state.extend(('finish_section', 'header_list', 'whitespace'))
5625 self.pending_body.part = tuple(self.parts)
5629 def state_finish_section(self, s):
5630 if not s.startswith(']'):
5631 raise Exception("section must end with ]")
5634 def state_header_list(self, s):
5635 if not s.startswith('('):
5636 raise Exception("Header list must begin with (")
5639 raise Exception("Header list must end with )")
5641 headers = s[1:end].split()
5642 self.pending_body.header.fields = map(str.upper, headers)
5645 def state_maybe_partial(self, s):
5646 # Grab <number.number> or nothing at all
5647 if not s.startswith('<'):
5651 raise Exception("Found < but not >")
5654 parts = partial.split('.', 1)
5656 raise Exception("Partial specification did not include two .-delimited integers")
5657 begin, length = map(int, parts)
5658 self.pending_body.partialBegin = begin
5659 self.pending_body.partialLength = length
5664 CHUNK_SIZE = 2 ** 2 ** 2 ** 2
5668 def __init__(self, f):
5671 def beginProducing(self, consumer):
5672 self.consumer = consumer
5673 self.produce = consumer.write
5674 d = self._onDone = defer.Deferred()
5675 self.consumer.registerProducer(self, False)
5678 def resumeProducing(self):
5681 b = '{%d}\r\n' % self._size()
5682 self.firstWrite = False
5685 b = b + self.f.read(self.CHUNK_SIZE)
5687 self.consumer.unregisterProducer()
5688 self._onDone.callback(self)
5689 self._onDone = self.f = self.consumer = None
5693 def pauseProducing(self):
5696 def stopProducing(self):
5707 # XXX - This may require localization :(
5709 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct',
5710 'nov', 'dec', 'january', 'february', 'march', 'april', 'may', 'june',
5711 'july', 'august', 'september', 'october', 'november', 'december'
5714 'day': r"(?P<day>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
5715 'mon': r"(?P<mon>\w+)",
5716 'year': r"(?P<year>\d\d\d\d)"
5718 m = re.match('%(day)s-%(mon)s-%(year)s' % expr, s)
5720 raise ValueError, "Cannot parse time string %r" % (s,)
5723 d['mon'] = 1 + (months.index(d['mon'].lower()) % 12)
5724 d['year'] = int(d['year'])
5725 d['day'] = int(d['day'])
5727 raise ValueError, "Cannot parse time string %r" % (s,)
5729 return time.struct_time(
5730 (d['year'], d['mon'], d['day'], 0, 0, 0, -1, -1, -1)
5734 def modified_base64(s):
5735 s_utf7 = s.encode('utf-7')
5736 return s_utf7[1:-1].replace('/', ',')
5738 def modified_unbase64(s):
5739 s_utf7 = '+' + s.replace(',', '/') + '-'
5740 return s_utf7.decode('utf-7')
5742 def encoder(s, errors=None):
5744 Encode the given C{unicode} string using the IMAP4 specific variation of
5748 @param s: The text to encode.
5750 @param errors: Policy for handling encoding errors. Currently ignored.
5752 @return: C{tuple} of a C{str} giving the encoded bytes and an C{int}
5753 giving the number of code units consumed from the input.
5758 if ord(c) in (range(0x20, 0x26) + range(0x27, 0x7f)):
5760 r.extend(['&', modified_base64(''.join(_in)), '-'])
5765 r.extend(['&', modified_base64(''.join(_in)), '-'])
5771 r.extend(['&', modified_base64(''.join(_in)), '-'])
5772 return (''.join(r), len(s))
5774 def decoder(s, errors=None):
5776 Decode the given C{str} using the IMAP4 specific variation of UTF-7.
5779 @param s: The bytes to decode.
5781 @param errors: Policy for handling decoding errors. Currently ignored.
5783 @return: a C{tuple} of a C{unicode} string giving the text which was
5784 decoded and an C{int} giving the number of bytes consumed from the
5790 if c == '&' and not decode:
5792 elif c == '-' and decode:
5793 if len(decode) == 1:
5796 r.append(modified_unbase64(''.join(decode[1:])))
5803 r.append(modified_unbase64(''.join(decode[1:])))
5804 return (''.join(r), len(s))
5806 class StreamReader(codecs.StreamReader):
5807 def decode(self, s, errors='strict'):
5810 class StreamWriter(codecs.StreamWriter):
5811 def encode(self, s, errors='strict'):
5814 _codecInfo = (encoder, decoder, StreamReader, StreamWriter)
5816 _codecInfoClass = codecs.CodecInfo
5817 except AttributeError:
5820 _codecInfo = _codecInfoClass(*_codecInfo)
5822 def imap4_utf_7(name):
5823 if name == 'imap4-utf-7':
5825 codecs.register(imap4_utf_7)
5829 'IMAP4Server', 'IMAP4Client',
5832 'IMailboxListener', 'IClientAuthentication', 'IAccount', 'IMailbox',
5833 'INamespacePresenter', 'ICloseableMailbox', 'IMailboxInfo',
5834 'IMessage', 'IMessageCopier', 'IMessageFile', 'ISearchableMailbox',
5837 'IMAP4Exception', 'IllegalClientResponse', 'IllegalOperation',
5838 'IllegalMailboxEncoding', 'UnhandledResponse', 'NegativeResponse',
5839 'NoSupportedAuthentication', 'IllegalServerResponse',
5840 'IllegalIdentifierError', 'IllegalQueryError', 'MismatchedNesting',
5841 'MismatchedQuoting', 'MailboxException', 'MailboxCollision',
5842 'NoSuchMailbox', 'ReadOnlyMailbox',
5845 'CramMD5ClientAuthenticator', 'PLAINAuthenticator', 'LOGINAuthenticator',
5846 'PLAINCredentials', 'LOGINCredentials',
5848 # Simple query interface
5849 'Query', 'Not', 'Or',
5853 'statusRequestHelper',