1
# -*- test-case-name: twisted.mail.test.test_imap -*-
2
# Copyright (c) 2001-2010 Twisted Matrix Laboratories.
3
# See LICENSE for details.
7
An IMAP4 protocol implementation
12
Suspend idle timeout while server is processing
13
Use an async message parser instead of buffering in memory
14
Figure out a way to not queue multi-message client requests (Flow? A simple callback?)
15
Clarify some API docs (Query, etc)
16
Make APPEND recognize (again) non-existent mailboxes before accepting the literal
34
import cStringIO as StringIO
38
from zope.interface import implements, Interface
40
from twisted.protocols import basic
41
from twisted.protocols import policies
42
from twisted.internet import defer
43
from twisted.internet import error
44
from twisted.internet.defer import maybeDeferred
45
from twisted.python import log, text
46
from twisted.internet import interfaces
48
from twisted import cred
49
import twisted.cred.error
50
import twisted.cred.credentials
54
class MessageSet(object):
56
Essentially an infinite bitfield, with some extra features.
58
@type getnext: Function taking C{int} returning C{int}
59
@ivar getnext: A function that returns the next message number,
60
used when iterating through the MessageSet. By default, a function
61
returning the next integer is supplied, but as this can be rather
62
inefficient for sparse UID iterations, it is recommended to supply
63
one when messages are requested by UID. The argument is provided
64
as a hint to the implementation and may be ignored if it makes sense
65
to do so (eg, if an iterator is being used that maintains its own
66
state, it is guaranteed that it will not be called out-of-order).
70
def __init__(self, start=_empty, end=_empty):
72
Create a new MessageSet()
74
@type start: Optional C{int}
75
@param start: Start of range, or only message number
77
@type end: Optional C{int}
78
@param end: End of range.
80
self._last = self._empty # Last message/UID in use
81
self.ranges = [] # List of ranges included
82
self.getnext = lambda x: x+1 # A function which will return the next
83
# message id. Handy for UID requests.
85
if start is self._empty:
88
if isinstance(start, types.ListType):
89
self.ranges = start[:]
96
def _setLast(self, value):
97
if self._last is not self._empty:
98
raise ValueError("last already set")
101
for i, (l, h) in enumerate(self.ranges):
103
break # There are no more Nones after this
109
self.ranges[i] = (l, h)
117
"Highest" message number, refered to by "*".
118
Must be set before attempting to use the MessageSet.
120
return _getLast, _setLast, None, doc
121
last = property(*last())
123
def add(self, start, end=_empty):
128
@param start: Start of range, or only message number
130
@type end: Optional C{int}
131
@param end: End of range.
133
if end is self._empty:
136
if self._last is not self._empty:
143
# Try to keep in low, high order if possible
144
# (But we don't know what None means, this will keep
145
# None at the start of the ranges list)
146
start, end = end, start
148
self.ranges.append((start, end))
151
def __add__(self, other):
152
if isinstance(other, MessageSet):
153
ranges = self.ranges + other.ranges
154
return MessageSet(ranges)
156
res = MessageSet(self.ranges)
164
def extend(self, other):
165
if isinstance(other, MessageSet):
166
self.ranges.extend(other.ranges)
179
Clean ranges list, combining adjacent ranges
184
oldl, oldh = None, None
185
for i,(l, h) in enumerate(self.ranges):
188
# l is >= oldl and h is >= oldh due to sort()
189
if oldl is not None and l <= oldh + 1:
192
self.ranges[i - 1] = None
193
self.ranges[i] = (l, h)
197
self.ranges = filter(None, self.ranges)
200
def __contains__(self, value):
202
May raise TypeError if we encounter unknown "high" values
204
for l, h in self.ranges:
207
"Can't determine membership; last value not set")
215
for l, h in self.ranges:
216
l = self.getnext(l-1)
224
if self.ranges and self.ranges[0][0] is None:
225
raise TypeError("Can't iterate; last value not set")
227
return self._iterator()
231
for l, h in self.ranges:
233
raise TypeError("Can't size object; last value not set")
240
for low, high in self.ranges:
247
p.append('%d:*' % (high,))
249
p.append('%d:%d' % (low, high))
253
return '<MessageSet %s>' % (str(self),)
255
def __eq__(self, other):
256
if isinstance(other, MessageSet):
257
return self.ranges == other.ranges
262
def __init__(self, size, defered):
267
def write(self, data):
268
self.size -= len(data)
271
self.data.append(data)
274
data, passon = data[:self.size], data[self.size:]
278
self.data.append(data)
281
def callback(self, line):
283
Call defered with data and rest of line
285
self.defer.callback((''.join(self.data), line))
288
_memoryFileLimit = 1024 * 1024 * 10
290
def __init__(self, size, defered):
293
if size > self._memoryFileLimit:
294
self.data = tempfile.TemporaryFile()
296
self.data = StringIO.StringIO()
298
def write(self, data):
299
self.size -= len(data)
302
self.data.write(data)
305
data, passon = data[:self.size], data[self.size:]
309
self.data.write(data)
312
def callback(self, line):
314
Call defered with data and rest of line
317
self.defer.callback((self.data, line))
321
"""Buffer up a bunch of writes before sending them all to a transport at once.
323
def __init__(self, transport, size=8192):
324
self.bufferSize = size
325
self.transport = transport
330
self._length += len(s)
331
self._writes.append(s)
332
if self._length > self.bufferSize:
337
self.transport.writeSequence(self._writes)
343
_1_RESPONSES = ('CAPABILITY', 'FLAGS', 'LIST', 'LSUB', 'STATUS', 'SEARCH', 'NAMESPACE')
344
_2_RESPONSES = ('EXISTS', 'EXPUNGE', 'FETCH', 'RECENT')
345
_OK_RESPONSES = ('UIDVALIDITY', 'UNSEEN', 'READ-WRITE', 'READ-ONLY', 'UIDNEXT', 'PERMANENTFLAGS')
348
def __init__(self, command, args=None, wantResponse=(),
349
continuation=None, *contArgs, **contKw):
350
self.command = command
352
self.wantResponse = wantResponse
353
self.continuation = lambda x: continuation(x, *contArgs, **contKw)
356
def format(self, tag):
357
if self.args is None:
358
return ' '.join((tag, self.command))
359
return ' '.join((tag, self.command, self.args))
361
def finish(self, lastLine, unusedCallback):
365
names = parseNestedParens(L)
367
if (N >= 1 and names[0] in self._1_RESPONSES or
368
N >= 2 and names[1] in self._2_RESPONSES or
369
N >= 2 and names[0] == 'OK' and isinstance(names[1], types.ListType) and names[1][0] in self._OK_RESPONSES):
373
d, self.defer = self.defer, None
374
d.callback((send, lastLine))
376
unusedCallback(unuse)
378
class LOGINCredentials(cred.credentials.UsernamePassword):
380
self.challenges = ['Password\0', 'User Name\0']
381
self.responses = ['password', 'username']
382
cred.credentials.UsernamePassword.__init__(self, None, None)
384
def getChallenge(self):
385
return self.challenges.pop()
387
def setResponse(self, response):
388
setattr(self, self.responses.pop(), response)
390
def moreChallenges(self):
391
return bool(self.challenges)
393
class PLAINCredentials(cred.credentials.UsernamePassword):
395
cred.credentials.UsernamePassword.__init__(self, None, None)
397
def getChallenge(self):
400
def setResponse(self, response):
401
parts = response.split('\0')
403
raise IllegalClientResponse("Malformed Response - wrong number of parts")
404
useless, self.username, self.password = parts
406
def moreChallenges(self):
409
class IMAP4Exception(Exception):
410
def __init__(self, *args):
411
Exception.__init__(self, *args)
413
class IllegalClientResponse(IMAP4Exception): pass
415
class IllegalOperation(IMAP4Exception): pass
417
class IllegalMailboxEncoding(IMAP4Exception): pass
419
class IMailboxListener(Interface):
420
"""Interface for objects interested in mailbox events"""
422
def modeChanged(writeable):
423
"""Indicates that the write status of a mailbox has changed.
425
@type writeable: C{bool}
426
@param writeable: A true value if write is now allowed, false
430
def flagsChanged(newFlags):
431
"""Indicates that the flags of one or more messages have changed.
433
@type newFlags: C{dict}
434
@param newFlags: A mapping of message identifiers to tuples of flags
435
now set on that message.
438
def newMessages(exists, recent):
439
"""Indicates that the number of messages in a mailbox has changed.
441
@type exists: C{int} or C{None}
442
@param exists: The total number of messages now in this mailbox.
443
If the total number of messages has not changed, this should be
447
@param recent: The number of messages now flagged \\Recent.
448
If the number of recent messages has not changed, this should be
452
class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin):
454
Protocol implementation for an IMAP4rev1 server.
456
The server can be in any of four states:
462
implements(IMailboxListener)
464
# Identifier for this server software
465
IDENT = 'Twisted IMAP4rev1 Ready'
467
# Number of seconds before idle timeout
468
# Initially 1 minute. Raised to 30 minutes after login.
471
POSTAUTH_TIMEOUT = 60 * 30
473
# Whether STARTTLS has been issued successfully yet or not.
476
# Whether our transport supports TLS
479
# Mapping of tags to commands we have received
482
# The object which will handle logins for us
485
# The account object for this connection
491
# The currently selected mailbox
494
# Command data to be processed when literal data is received
495
_pendingLiteral = None
497
# Maximum length to accept for a "short" string literal
498
_literalStringLimit = 4096
500
# IChallengeResponse factories for AUTHENTICATE command
503
# Search terms the implementation of which needs to be passed the
504
# last sequence id value.
505
_requiresLastSequenceId = set(["OR", "NOT"])
509
parseState = 'command'
511
def __init__(self, chal = None, contextFactory = None, scheduler = None):
514
self.challengers = chal
515
self.ctx = contextFactory
516
if scheduler is None:
517
scheduler = iterateInReactor
518
self._scheduler = scheduler
519
self._queuedAsync = []
521
def capabilities(self):
522
cap = {'AUTH': self.challengers.keys()}
523
if self.ctx and self.canStartTLS:
524
if not self.startedTLS and interfaces.ISSLTransport(self.transport, None) is None:
525
cap['LOGINDISABLED'] = None
526
cap['STARTTLS'] = None
527
cap['NAMESPACE'] = None
531
def connectionMade(self):
533
self.canStartTLS = interfaces.ITLSTransport(self.transport, None) is not None
534
self.setTimeout(self.timeOut)
535
self.sendServerGreeting()
537
def connectionLost(self, reason):
538
self.setTimeout(None)
541
self._onLogout = None
543
def timeoutConnection(self):
544
self.sendLine('* BYE Autologout; connection idle too long')
545
self.transport.loseConnection()
547
self.mbox.removeListener(self)
548
cmbx = ICloseableMailbox(self.mbox, None)
550
maybeDeferred(cmbx.close).addErrback(log.err)
552
self.state = 'timeout'
554
def rawDataReceived(self, data):
556
passon = self._pendingLiteral.write(data)
557
if passon is not None:
558
self.setLineMode(passon)
560
# Avoid processing commands while buffers are being dumped to
565
commands = self.blocked
567
while commands and self.blocked is None:
568
self.lineReceived(commands.pop(0))
569
if self.blocked is not None:
570
self.blocked.extend(commands)
572
def lineReceived(self, line):
573
if self.blocked is not None:
574
self.blocked.append(line)
579
f = getattr(self, 'parse_' + self.parseState)
583
self.sendUntaggedResponse('BAD Server error: ' + str(e))
586
def parse_command(self, line):
587
args = line.split(None, 2)
590
tag, cmd, rest = args
595
self.sendBadResponse(tag, 'Missing command')
598
self.sendBadResponse(None, 'Null command')
603
return self.dispatchCommand(tag, cmd, rest)
604
except IllegalClientResponse, e:
605
self.sendBadResponse(tag, 'Illegal syntax: ' + str(e))
606
except IllegalOperation, e:
607
self.sendNegativeResponse(tag, 'Illegal operation: ' + str(e))
608
except IllegalMailboxEncoding, e:
609
self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + str(e))
611
def parse_pending(self, line):
612
d = self._pendingLiteral
613
self._pendingLiteral = None
614
self.parseState = 'command'
617
def dispatchCommand(self, tag, cmd, rest, uid=None):
618
f = self.lookupCommand(cmd)
622
self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid)
624
self.sendBadResponse(tag, 'Unsupported command')
626
def lookupCommand(self, cmd):
627
return getattr(self, '_'.join((self.state, cmd.upper())), None)
629
def __doCommand(self, tag, handler, args, parseargs, line, uid):
630
for (i, arg) in enumerate(parseargs):
632
parseargs = parseargs[i+1:]
633
maybeDeferred(arg, self, line).addCallback(
634
self.__cbDispatch, tag, handler, args,
635
parseargs, uid).addErrback(self.__ebDispatch, tag)
642
raise IllegalClientResponse("Too many arguments for command: " + repr(line))
645
handler(uid=uid, *args)
649
def __cbDispatch(self, (arg, rest), tag, fn, args, parseargs, uid):
651
self.__doCommand(tag, fn, args, parseargs, rest, uid)
653
def __ebDispatch(self, failure, tag):
654
if failure.check(IllegalClientResponse):
655
self.sendBadResponse(tag, 'Illegal syntax: ' + str(failure.value))
656
elif failure.check(IllegalOperation):
657
self.sendNegativeResponse(tag, 'Illegal operation: ' +
659
elif failure.check(IllegalMailboxEncoding):
660
self.sendNegativeResponse(tag, 'Illegal mailbox name: ' +
663
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
666
def _stringLiteral(self, size):
667
if size > self._literalStringLimit:
668
raise IllegalClientResponse(
669
"Literal too long! I accept at most %d octets" %
670
(self._literalStringLimit,))
672
self.parseState = 'pending'
673
self._pendingLiteral = LiteralString(size, d)
674
self.sendContinuationRequest('Ready for %d octets of text' % size)
678
def _fileLiteral(self, size):
680
self.parseState = 'pending'
681
self._pendingLiteral = LiteralFile(size, d)
682
self.sendContinuationRequest('Ready for %d octets of data' % size)
686
def arg_astring(self, line):
688
Parse an astring from the line, return (arg, rest), possibly
689
via a deferred (to handle literals)
693
raise IllegalClientResponse("Missing argument")
695
arg, rest = None, None
698
spam, arg, rest = line.split('"',2)
699
rest = rest[1:] # Strip space
701
raise IllegalClientResponse("Unmatched quotes")
705
raise IllegalClientResponse("Malformed literal")
707
size = int(line[1:-1])
709
raise IllegalClientResponse("Bad literal size: " + line[1:-1])
710
d = self._stringLiteral(size)
712
arg = line.split(' ',1)
716
return d or (arg, rest)
718
# ATOM: Any CHAR except ( ) { % * " \ ] CTL SP (CHAR is 7bit)
719
atomre = re.compile(r'(?P<atom>[^\](){%*"\\\x00-\x20\x80-\xff]+)( (?P<rest>.*$)|$)')
721
def arg_atom(self, line):
723
Parse an atom from the line
726
raise IllegalClientResponse("Missing argument")
727
m = self.atomre.match(line)
729
return m.group('atom'), m.group('rest')
731
raise IllegalClientResponse("Malformed ATOM")
733
def arg_plist(self, line):
735
Parse a (non-nested) parenthesised list from the line
738
raise IllegalClientResponse("Missing argument")
741
raise IllegalClientResponse("Missing parenthesis")
746
raise IllegalClientResponse("Mismatched parenthesis")
748
return (parseNestedParens(line[1:i],0), line[i+2:])
750
def arg_literal(self, line):
752
Parse a literal from the line
755
raise IllegalClientResponse("Missing argument")
758
raise IllegalClientResponse("Missing literal")
761
raise IllegalClientResponse("Malformed literal")
764
size = int(line[1:-1])
766
raise IllegalClientResponse("Bad literal size: " + line[1:-1])
768
return self._fileLiteral(size)
770
def arg_searchkeys(self, line):
774
query = parseNestedParens(line)
775
# XXX Should really use list of search terms and parse into
780
def arg_seqset(self, line):
785
arg = line.split(' ',1)
791
return (parseIdList(arg), rest)
792
except IllegalIdentifierError, e:
793
raise IllegalClientResponse("Bad message number " + str(e))
795
def arg_fetchatt(self, line):
801
return (p.result, '')
803
def arg_flaglist(self, line):
805
Flag part of store-att-flag
810
raise IllegalClientResponse("Mismatched parenthesis")
814
m = self.atomre.search(line)
816
raise IllegalClientResponse("Malformed flag")
817
if line[0] == '\\' and m.start() == 1:
818
flags.append('\\' + m.group('atom'))
820
flags.append(m.group('atom'))
822
raise IllegalClientResponse("Malformed flag")
823
line = m.group('rest')
827
def arg_line(self, line):
829
Command line of UID command
833
def opt_plist(self, line):
835
Optional parenthesised list
837
if line.startswith('('):
838
return self.arg_plist(line)
842
def opt_datetime(self, line):
844
Optional date-time string
846
if line.startswith('"'):
848
spam, date, rest = line.split('"',2)
850
raise IllegalClientResponse("Malformed date-time")
851
return (date, rest[1:])
855
def opt_charset(self, line):
857
Optional charset of SEARCH command
859
if line[:7].upper() == 'CHARSET':
860
arg = line.split(' ',2)
862
raise IllegalClientResponse("Missing charset identifier")
865
spam, arg, rest = arg
870
def sendServerGreeting(self):
871
msg = '[CAPABILITY %s] %s' % (' '.join(self.listCapabilities()), self.IDENT)
872
self.sendPositiveResponse(message=msg)
874
def sendBadResponse(self, tag = None, message = ''):
875
self._respond('BAD', tag, message)
877
def sendPositiveResponse(self, tag = None, message = ''):
878
self._respond('OK', tag, message)
880
def sendNegativeResponse(self, tag = None, message = ''):
881
self._respond('NO', tag, message)
883
def sendUntaggedResponse(self, message, async=False):
884
if not async or (self.blocked is None):
885
self._respond(message, None, None)
887
self._queuedAsync.append(message)
889
def sendContinuationRequest(self, msg = 'Ready for additional command text'):
891
self.sendLine('+ ' + msg)
895
def _respond(self, state, tag, message):
896
if state in ('OK', 'NO', 'BAD') and self._queuedAsync:
897
lines = self._queuedAsync
898
self._queuedAsync = []
900
self._respond(msg, None, None)
904
self.sendLine(' '.join((tag, state, message)))
906
self.sendLine(' '.join((tag, state)))
908
def listCapabilities(self):
910
for c, v in self.capabilities().iteritems():
914
caps.extend([('%s=%s' % (c, cap)) for cap in v])
917
def do_CAPABILITY(self, tag):
918
self.sendUntaggedResponse('CAPABILITY ' + ' '.join(self.listCapabilities()))
919
self.sendPositiveResponse(tag, 'CAPABILITY completed')
921
unauth_CAPABILITY = (do_CAPABILITY,)
922
auth_CAPABILITY = unauth_CAPABILITY
923
select_CAPABILITY = unauth_CAPABILITY
924
logout_CAPABILITY = unauth_CAPABILITY
926
def do_LOGOUT(self, tag):
927
self.sendUntaggedResponse('BYE Nice talking to you')
928
self.sendPositiveResponse(tag, 'LOGOUT successful')
929
self.transport.loseConnection()
931
unauth_LOGOUT = (do_LOGOUT,)
932
auth_LOGOUT = unauth_LOGOUT
933
select_LOGOUT = unauth_LOGOUT
934
logout_LOGOUT = unauth_LOGOUT
936
def do_NOOP(self, tag):
937
self.sendPositiveResponse(tag, 'NOOP No operation performed')
939
unauth_NOOP = (do_NOOP,)
940
auth_NOOP = unauth_NOOP
941
select_NOOP = unauth_NOOP
942
logout_NOOP = unauth_NOOP
944
def do_AUTHENTICATE(self, tag, args):
945
args = args.upper().strip()
946
if args not in self.challengers:
947
self.sendNegativeResponse(tag, 'AUTHENTICATE method unsupported')
949
self.authenticate(self.challengers[args](), tag)
951
unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom)
953
def authenticate(self, chal, tag):
954
if self.portal is None:
955
self.sendNegativeResponse(tag, 'Temporary authentication failure')
958
self._setupChallenge(chal, tag)
960
def _setupChallenge(self, chal, tag):
962
challenge = chal.getChallenge()
964
self.sendBadResponse(tag, 'Server error: ' + str(e))
966
coded = base64.encodestring(challenge)[:-1]
967
self.parseState = 'pending'
968
self._pendingLiteral = defer.Deferred()
969
self.sendContinuationRequest(coded)
970
self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag)
971
self._pendingLiteral.addErrback(self.__ebAuthChunk, tag)
973
def __cbAuthChunk(self, result, chal, tag):
975
uncoded = base64.decodestring(result)
976
except binascii.Error:
977
raise IllegalClientResponse("Malformed Response - not base64")
979
chal.setResponse(uncoded)
980
if chal.moreChallenges():
981
self._setupChallenge(chal, tag)
983
self.portal.login(chal, None, IAccount).addCallbacks(
986
(tag,), None, (tag,), None
989
def __cbAuthResp(self, (iface, avatar, logout), tag):
990
assert iface is IAccount, "IAccount is the only supported interface"
991
self.account = avatar
993
self._onLogout = logout
994
self.sendPositiveResponse(tag, 'Authentication successful')
995
self.setTimeout(self.POSTAUTH_TIMEOUT)
997
def __ebAuthResp(self, failure, tag):
998
if failure.check(cred.error.UnauthorizedLogin):
999
self.sendNegativeResponse(tag, 'Authentication failed: unauthorized')
1000
elif failure.check(cred.error.UnhandledCredentials):
1001
self.sendNegativeResponse(tag, 'Authentication failed: server misconfigured')
1003
self.sendBadResponse(tag, 'Server error: login failed unexpectedly')
1006
def __ebAuthChunk(self, failure, tag):
1007
self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.value))
1009
def do_STARTTLS(self, tag):
1011
self.sendNegativeResponse(tag, 'TLS already negotiated')
1012
elif self.ctx and self.canStartTLS:
1013
self.sendPositiveResponse(tag, 'Begin TLS negotiation now')
1014
self.transport.startTLS(self.ctx)
1015
self.startedTLS = True
1016
self.challengers = self.challengers.copy()
1017
if 'LOGIN' not in self.challengers:
1018
self.challengers['LOGIN'] = LOGINCredentials
1019
if 'PLAIN' not in self.challengers:
1020
self.challengers['PLAIN'] = PLAINCredentials
1022
self.sendNegativeResponse(tag, 'TLS not available')
1024
unauth_STARTTLS = (do_STARTTLS,)
1026
def do_LOGIN(self, tag, user, passwd):
1027
if 'LOGINDISABLED' in self.capabilities():
1028
self.sendBadResponse(tag, 'LOGIN is disabled before STARTTLS')
1031
maybeDeferred(self.authenticateLogin, user, passwd
1032
).addCallback(self.__cbLogin, tag
1033
).addErrback(self.__ebLogin, tag
1036
unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring)
1038
def authenticateLogin(self, user, passwd):
1039
"""Lookup the account associated with the given parameters
1041
Override this method to define the desired authentication behavior.
1043
The default behavior is to defer authentication to C{self.portal}
1044
if it is not None, or to deny the login otherwise.
1047
@param user: The username to lookup
1049
@type passwd: C{str}
1050
@param passwd: The password to login with
1053
return self.portal.login(
1054
cred.credentials.UsernamePassword(user, passwd),
1057
raise cred.error.UnauthorizedLogin()
1059
def __cbLogin(self, (iface, avatar, logout), tag):
1060
if iface is not IAccount:
1061
self.sendBadResponse(tag, 'Server error: login returned unexpected value')
1062
log.err("__cbLogin called with %r, IAccount expected" % (iface,))
1064
self.account = avatar
1065
self._onLogout = logout
1066
self.sendPositiveResponse(tag, 'LOGIN succeeded')
1068
self.setTimeout(self.POSTAUTH_TIMEOUT)
1070
def __ebLogin(self, failure, tag):
1071
if failure.check(cred.error.UnauthorizedLogin):
1072
self.sendNegativeResponse(tag, 'LOGIN failed')
1074
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1077
def do_NAMESPACE(self, tag):
1078
personal = public = shared = None
1079
np = INamespacePresenter(self.account, None)
1081
personal = np.getPersonalNamespaces()
1082
public = np.getSharedNamespaces()
1083
shared = np.getSharedNamespaces()
1084
self.sendUntaggedResponse('NAMESPACE ' + collapseNestedLists([personal, public, shared]))
1085
self.sendPositiveResponse(tag, "NAMESPACE command completed")
1087
auth_NAMESPACE = (do_NAMESPACE,)
1088
select_NAMESPACE = auth_NAMESPACE
1090
def _parseMbox(self, name):
1091
if isinstance(name, unicode):
1094
return name.decode('imap4-utf-7')
1097
raise IllegalMailboxEncoding(name)
1099
def _selectWork(self, tag, name, rw, cmdName):
1101
self.mbox.removeListener(self)
1102
cmbx = ICloseableMailbox(self.mbox, None)
1103
if cmbx is not None:
1104
maybeDeferred(cmbx.close).addErrback(log.err)
1108
name = self._parseMbox(name)
1109
maybeDeferred(self.account.select, self._parseMbox(name), rw
1110
).addCallback(self._cbSelectWork, cmdName, tag
1111
).addErrback(self._ebSelectWork, cmdName, tag
1114
def _ebSelectWork(self, failure, cmdName, tag):
1115
self.sendBadResponse(tag, "%s failed: Server error" % (cmdName,))
1118
def _cbSelectWork(self, mbox, cmdName, tag):
1120
self.sendNegativeResponse(tag, 'No such mailbox')
1122
if '\\noselect' in [s.lower() for s in mbox.getFlags()]:
1123
self.sendNegativeResponse(tag, 'Mailbox cannot be selected')
1126
flags = mbox.getFlags()
1127
self.sendUntaggedResponse(str(mbox.getMessageCount()) + ' EXISTS')
1128
self.sendUntaggedResponse(str(mbox.getRecentCount()) + ' RECENT')
1129
self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags))
1130
self.sendPositiveResponse(None, '[UIDVALIDITY %d]' % mbox.getUIDValidity())
1132
s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY'
1133
mbox.addListener(self)
1134
self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName))
1135
self.state = 'select'
1138
auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' )
1139
select_SELECT = auth_SELECT
1141
auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' )
1142
select_EXAMINE = auth_EXAMINE
1145
def do_IDLE(self, tag):
1146
self.sendContinuationRequest(None)
1148
self.lastState = self.parseState
1149
self.parseState = 'idle'
1151
def parse_idle(self, *args):
1152
self.parseState = self.lastState
1154
self.sendPositiveResponse(self.parseTag, "IDLE terminated")
1157
select_IDLE = ( do_IDLE, )
1158
auth_IDLE = select_IDLE
1161
def do_CREATE(self, tag, name):
1162
name = self._parseMbox(name)
1164
result = self.account.create(name)
1165
except MailboxException, c:
1166
self.sendNegativeResponse(tag, str(c))
1168
self.sendBadResponse(tag, "Server error encountered while creating mailbox")
1172
self.sendPositiveResponse(tag, 'Mailbox created')
1174
self.sendNegativeResponse(tag, 'Mailbox not created')
1176
auth_CREATE = (do_CREATE, arg_astring)
1177
select_CREATE = auth_CREATE
1179
def do_DELETE(self, tag, name):
1180
name = self._parseMbox(name)
1181
if name.lower() == 'inbox':
1182
self.sendNegativeResponse(tag, 'You cannot delete the inbox')
1185
self.account.delete(name)
1186
except MailboxException, m:
1187
self.sendNegativeResponse(tag, str(m))
1189
self.sendBadResponse(tag, "Server error encountered while deleting mailbox")
1192
self.sendPositiveResponse(tag, 'Mailbox deleted')
1194
auth_DELETE = (do_DELETE, arg_astring)
1195
select_DELETE = auth_DELETE
1197
def do_RENAME(self, tag, oldname, newname):
1198
oldname, newname = [self._parseMbox(n) for n in oldname, newname]
1199
if oldname.lower() == 'inbox' or newname.lower() == 'inbox':
1200
self.sendNegativeResponse(tag, 'You cannot rename the inbox, or rename another mailbox to inbox.')
1203
self.account.rename(oldname, newname)
1205
self.sendBadResponse(tag, 'Invalid command syntax')
1206
except MailboxException, m:
1207
self.sendNegativeResponse(tag, str(m))
1209
self.sendBadResponse(tag, "Server error encountered while renaming mailbox")
1212
self.sendPositiveResponse(tag, 'Mailbox renamed')
1214
auth_RENAME = (do_RENAME, arg_astring, arg_astring)
1215
select_RENAME = auth_RENAME
1217
def do_SUBSCRIBE(self, tag, name):
1218
name = self._parseMbox(name)
1220
self.account.subscribe(name)
1221
except MailboxException, m:
1222
self.sendNegativeResponse(tag, str(m))
1224
self.sendBadResponse(tag, "Server error encountered while subscribing to mailbox")
1227
self.sendPositiveResponse(tag, 'Subscribed')
1229
auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring)
1230
select_SUBSCRIBE = auth_SUBSCRIBE
1232
def do_UNSUBSCRIBE(self, tag, name):
1233
name = self._parseMbox(name)
1235
self.account.unsubscribe(name)
1236
except MailboxException, m:
1237
self.sendNegativeResponse(tag, str(m))
1239
self.sendBadResponse(tag, "Server error encountered while unsubscribing from mailbox")
1242
self.sendPositiveResponse(tag, 'Unsubscribed')
1244
auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring)
1245
select_UNSUBSCRIBE = auth_UNSUBSCRIBE
1247
def _listWork(self, tag, ref, mbox, sub, cmdName):
1248
mbox = self._parseMbox(mbox)
1249
maybeDeferred(self.account.listMailboxes, ref, mbox
1250
).addCallback(self._cbListWork, tag, sub, cmdName
1251
).addErrback(self._ebListWork, tag
1254
def _cbListWork(self, mailboxes, tag, sub, cmdName):
1255
for (name, box) in mailboxes:
1256
if not sub or self.account.isSubscribed(name):
1257
flags = box.getFlags()
1258
delim = box.getHierarchicalDelimiter()
1259
resp = (DontQuoteMe(cmdName), map(DontQuoteMe, flags), delim, name.encode('imap4-utf-7'))
1260
self.sendUntaggedResponse(collapseNestedLists(resp))
1261
self.sendPositiveResponse(tag, '%s completed' % (cmdName,))
1263
def _ebListWork(self, failure, tag):
1264
self.sendBadResponse(tag, "Server error encountered while listing mailboxes.")
1267
auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST')
1268
select_LIST = auth_LIST
1270
auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB')
1271
select_LSUB = auth_LSUB
1273
def do_STATUS(self, tag, mailbox, names):
1274
mailbox = self._parseMbox(mailbox)
1275
maybeDeferred(self.account.select, mailbox, 0
1276
).addCallback(self._cbStatusGotMailbox, tag, mailbox, names
1277
).addErrback(self._ebStatusGotMailbox, tag
1280
def _cbStatusGotMailbox(self, mbox, tag, mailbox, names):
1282
maybeDeferred(mbox.requestStatus, names).addCallbacks(
1283
self.__cbStatus, self.__ebStatus,
1284
(tag, mailbox), None, (tag, mailbox), None
1287
self.sendNegativeResponse(tag, "Could not open mailbox")
1289
def _ebStatusGotMailbox(self, failure, tag):
1290
self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
1293
auth_STATUS = (do_STATUS, arg_astring, arg_plist)
1294
select_STATUS = auth_STATUS
1296
def __cbStatus(self, status, tag, box):
1297
line = ' '.join(['%s %s' % x for x in status.iteritems()])
1298
self.sendUntaggedResponse('STATUS %s (%s)' % (box, line))
1299
self.sendPositiveResponse(tag, 'STATUS complete')
1301
def __ebStatus(self, failure, tag, box):
1302
self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, str(failure.value)))
1304
def do_APPEND(self, tag, mailbox, flags, date, message):
1305
mailbox = self._parseMbox(mailbox)
1306
maybeDeferred(self.account.select, mailbox
1307
).addCallback(self._cbAppendGotMailbox, tag, flags, date, message
1308
).addErrback(self._ebAppendGotMailbox, tag
1311
def _cbAppendGotMailbox(self, mbox, tag, flags, date, message):
1313
self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox')
1316
d = mbox.addMessage(message, flags, date)
1317
d.addCallback(self.__cbAppend, tag, mbox)
1318
d.addErrback(self.__ebAppend, tag)
1320
def _ebAppendGotMailbox(self, failure, tag):
1321
self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
1324
auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime,
1326
select_APPEND = auth_APPEND
1328
def __cbAppend(self, result, tag, mbox):
1329
self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount())
1330
self.sendPositiveResponse(tag, 'APPEND complete')
1332
def __ebAppend(self, failure, tag):
1333
self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value))
1335
def do_CHECK(self, tag):
1336
d = self.checkpoint()
1338
self.__cbCheck(None, tag)
1343
callbackArgs=(tag,),
1346
select_CHECK = (do_CHECK,)
1348
def __cbCheck(self, result, tag):
1349
self.sendPositiveResponse(tag, 'CHECK completed')
1351
def __ebCheck(self, failure, tag):
1352
self.sendBadResponse(tag, 'CHECK failed: ' + str(failure.value))
1354
def checkpoint(self):
1355
"""Called when the client issues a CHECK command.
1357
This should perform any checkpoint operations required by the server.
1358
It may be a long running operation, but may not block. If it returns
1359
a deferred, the client will only be informed of success (or failure)
1360
when the deferred's callback (or errback) is invoked.
1364
def do_CLOSE(self, tag):
1366
if self.mbox.isWriteable():
1367
d = maybeDeferred(self.mbox.expunge)
1368
cmbx = ICloseableMailbox(self.mbox, None)
1369
if cmbx is not None:
1371
d.addCallback(lambda result: cmbx.close())
1373
d = maybeDeferred(cmbx.close)
1375
d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,), None)
1377
self.__cbClose(None, tag)
1379
select_CLOSE = (do_CLOSE,)
1381
def __cbClose(self, result, tag):
1382
self.sendPositiveResponse(tag, 'CLOSE completed')
1383
self.mbox.removeListener(self)
1387
def __ebClose(self, failure, tag):
1388
self.sendBadResponse(tag, 'CLOSE failed: ' + str(failure.value))
1390
def do_EXPUNGE(self, tag):
1391
if self.mbox.isWriteable():
1392
maybeDeferred(self.mbox.expunge).addCallbacks(
1393
self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None
1396
self.sendNegativeResponse(tag, 'EXPUNGE ignored on read-only mailbox')
1398
select_EXPUNGE = (do_EXPUNGE,)
1400
def __cbExpunge(self, result, tag):
1402
self.sendUntaggedResponse('%d EXPUNGE' % e)
1403
self.sendPositiveResponse(tag, 'EXPUNGE completed')
1405
def __ebExpunge(self, failure, tag):
1406
self.sendBadResponse(tag, 'EXPUNGE failed: ' + str(failure.value))
1409
def do_SEARCH(self, tag, charset, query, uid=0):
1410
sm = ISearchableMailbox(self.mbox, None)
1412
maybeDeferred(sm.search, query, uid=uid).addCallbacks(
1413
self.__cbSearch, self.__ebSearch,
1414
(tag, self.mbox, uid), None, (tag,), None
1417
s = parseIdList('1:*')
1418
maybeDeferred(self.mbox.fetch, s, uid=uid).addCallbacks(
1419
self.__cbManualSearch, self.__ebSearch,
1420
(tag, self.mbox, query, uid), None, (tag,), None
1423
select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys)
1425
def __cbSearch(self, result, tag, mbox, uid):
1427
result = map(mbox.getUID, result)
1428
ids = ' '.join([str(i) for i in result])
1429
self.sendUntaggedResponse('SEARCH ' + ids)
1430
self.sendPositiveResponse(tag, 'SEARCH completed')
1433
def __cbManualSearch(self, result, tag, mbox, query, uid,
1434
searchResults=None):
1435
if searchResults is None:
1439
lastSequenceId = result[-1][0]
1441
for (i, (id, msg)) in zip(range(5), result):
1442
# searchFilter and singleSearchStep will mutate the query. Dang.
1443
# Copy it here or else things will go poorly for subsequent
1445
if self._searchFilter(copy.deepcopy(query), id, msg, lastSequenceId):
1447
searchResults.append(str(msg.getUID()))
1449
searchResults.append(str(id))
1451
from twisted.internet import reactor
1453
0, self.__cbManualSearch, result, tag, mbox, query, uid,
1457
self.sendUntaggedResponse('SEARCH ' + ' '.join(searchResults))
1458
self.sendPositiveResponse(tag, 'SEARCH completed')
1461
def _searchFilter(self, query, id, msg, lastSequenceId):
1463
Pop search terms from the beginning of C{query} until there are none
1464
left and apply them to the given message.
1466
@param query: A list representing the parsed form of the search query.
1468
@param id: The sequence number of the message being checked.
1470
@param msg: The message being checked.
1472
@param lastSequenceId: The highest sequence number of any message in
1473
the mailbox being searched.
1475
@return: Boolean indicating whether all of the query terms match the
1479
if not self._singleSearchStep(query, id, msg, lastSequenceId):
1484
def _singleSearchStep(self, query, id, msg, lastSequenceId):
1486
Pop one search term from the beginning of C{query} (possibly more than
1487
one element) and return whether it matches the given message.
1489
@param query: A list representing the parsed form of the search query.
1491
@param id: The sequence number of the message being checked.
1493
@param msg: The message being checked.
1495
@param lastSequenceId: The highest sequence number of any message in
1496
the mailbox being searched.
1498
@return: Boolean indicating whether the query term matched the message.
1501
if isinstance(q, list):
1502
if not self._searchFilter(q, id, msg, lastSequenceId):
1506
if not c[:1].isalpha():
1507
# A search term may be a word like ALL, ANSWERED, BCC, etc (see
1508
# below) or it may be a message sequence set. Here we
1509
# recognize a message sequence set "N:M".
1510
messageSet = parseIdList(c)
1511
messageSet.last = lastSequenceId
1512
return id in messageSet
1514
f = getattr(self, 'search_' + c)
1516
if c in self._requiresLastSequenceId:
1517
result = f(query, id, msg, lastSequenceId)
1519
result = f(query, id, msg)
1524
def search_ALL(self, query, id, msg):
1527
def search_ANSWERED(self, query, id, msg):
1528
return '\\Answered' in msg.getFlags()
1530
def search_BCC(self, query, id, msg):
1531
bcc = msg.getHeaders(False, 'bcc').get('bcc', '')
1532
return bcc.lower().find(query.pop(0).lower()) != -1
1534
def search_BEFORE(self, query, id, msg):
1535
date = parseTime(query.pop(0))
1536
return rfc822.parsedate(msg.getInternalDate()) < date
1538
def search_BODY(self, query, id, msg):
1539
body = query.pop(0).lower()
1540
return text.strFile(body, msg.getBodyFile(), False)
1542
def search_CC(self, query, id, msg):
1543
cc = msg.getHeaders(False, 'cc').get('cc', '')
1544
return cc.lower().find(query.pop(0).lower()) != -1
1546
def search_DELETED(self, query, id, msg):
1547
return '\\Deleted' in msg.getFlags()
1549
def search_DRAFT(self, query, id, msg):
1550
return '\\Draft' in msg.getFlags()
1552
def search_FLAGGED(self, query, id, msg):
1553
return '\\Flagged' in msg.getFlags()
1555
def search_FROM(self, query, id, msg):
1556
fm = msg.getHeaders(False, 'from').get('from', '')
1557
return fm.lower().find(query.pop(0).lower()) != -1
1559
def search_HEADER(self, query, id, msg):
1560
hdr = query.pop(0).lower()
1561
hdr = msg.getHeaders(False, hdr).get(hdr, '')
1562
return hdr.lower().find(query.pop(0).lower()) != -1
1564
def search_KEYWORD(self, query, id, msg):
1568
def search_LARGER(self, query, id, msg):
1569
return int(query.pop(0)) < msg.getSize()
1571
def search_NEW(self, query, id, msg):
1572
return '\\Recent' in msg.getFlags() and '\\Seen' not in msg.getFlags()
1574
def search_NOT(self, query, id, msg, lastSequenceId):
1575
return not self._singleSearchStep(query, id, msg, lastSequenceId)
1577
def search_OLD(self, query, id, msg):
1578
return '\\Recent' not in msg.getFlags()
1580
def search_ON(self, query, id, msg):
1581
date = parseTime(query.pop(0))
1582
return rfc822.parsedate(msg.getInternalDate()) == date
1584
def search_OR(self, query, id, msg, lastSequenceId):
1585
a = self._singleSearchStep(query, id, msg, lastSequenceId)
1586
b = self._singleSearchStep(query, id, msg, lastSequenceId)
1589
def search_RECENT(self, query, id, msg):
1590
return '\\Recent' in msg.getFlags()
1592
def search_SEEN(self, query, id, msg):
1593
return '\\Seen' in msg.getFlags()
1595
def search_SENTBEFORE(self, query, id, msg):
1597
Returns C{True} if the message date is earlier than the query date.
1599
@type query: A C{list} of C{str}
1600
@param query: A list whose first element starts with a stringified date
1601
that is a fragment of an L{imap4.Query()}. The date must be in the
1602
format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
1604
@type msg: Provider of L{imap4.IMessage}
1606
date = msg.getHeaders(False, 'date').get('date', '')
1607
date = rfc822.parsedate(date)
1608
return date < parseTime(query.pop(0))
1610
def search_SENTON(self, query, id, msg):
1612
Returns C{True} if the message date is the same as the query date.
1614
@type query: A C{list} of C{str}
1615
@param query: A list whose first element starts with a stringified date
1616
that is a fragment of an L{imap4.Query()}. The date must be in the
1617
format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
1619
@type msg: Provider of L{imap4.IMessage}
1621
date = msg.getHeaders(False, 'date').get('date', '')
1622
date = rfc822.parsedate(date)
1623
return date[:3] == parseTime(query.pop(0))[:3]
1625
def search_SENTSINCE(self, query, id, msg):
1627
Returns C{True} if the message date is later than the query date.
1629
@type query: A C{list} of C{str}
1630
@param query: A list whose first element starts with a stringified date
1631
that is a fragment of an L{imap4.Query()}. The date must be in the
1632
format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
1634
@type msg: Provider of L{imap4.IMessage}
1636
date = msg.getHeaders(False, 'date').get('date', '')
1637
date = rfc822.parsedate(date)
1638
return date > parseTime(query.pop(0))
1640
def search_SINCE(self, query, id, msg):
1641
date = parseTime(query.pop(0))
1642
return rfc822.parsedate(msg.getInternalDate()) > date
1644
def search_SMALLER(self, query, id, msg):
1645
return int(query.pop(0)) > msg.getSize()
1647
def search_SUBJECT(self, query, id, msg):
1648
subj = msg.getHeaders(False, 'subject').get('subject', '')
1649
return subj.lower().find(query.pop(0).lower()) != -1
1651
def search_TEXT(self, query, id, msg):
1652
# XXX - This must search headers too
1653
body = query.pop(0).lower()
1654
return text.strFile(body, msg.getBodyFile(), False)
1656
def search_TO(self, query, id, msg):
1657
to = msg.getHeaders(False, 'to').get('to', '')
1658
return to.lower().find(query.pop(0).lower()) != -1
1660
def search_UID(self, query, id, msg):
1663
return msg.getUID() in m
1665
def search_UNANSWERED(self, query, id, msg):
1666
return '\\Answered' not in msg.getFlags()
1668
def search_UNDELETED(self, query, id, msg):
1669
return '\\Deleted' not in msg.getFlags()
1671
def search_UNDRAFT(self, query, id, msg):
1672
return '\\Draft' not in msg.getFlags()
1674
def search_UNFLAGGED(self, query, id, msg):
1675
return '\\Flagged' not in msg.getFlags()
1677
def search_UNKEYWORD(self, query, id, msg):
1681
def search_UNSEEN(self, query, id, msg):
1682
return '\\Seen' not in msg.getFlags()
1684
def __ebSearch(self, failure, tag):
1685
self.sendBadResponse(tag, 'SEARCH failed: ' + str(failure.value))
1688
def do_FETCH(self, tag, messages, query, uid=0):
1690
self._oldTimeout = self.setTimeout(None)
1691
maybeDeferred(self.mbox.fetch, messages, uid=uid
1693
).addCallback(self.__cbFetch, tag, query, uid
1694
).addErrback(self.__ebFetch, tag
1697
self.sendPositiveResponse(tag, 'FETCH complete')
1699
select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt)
1701
def __cbFetch(self, results, tag, query, uid):
1702
if self.blocked is None:
1705
id, msg = results.next()
1706
except StopIteration:
1707
# The idle timeout was suspended while we delivered results,
1709
self.setTimeout(self._oldTimeout)
1710
del self._oldTimeout
1712
# All results have been processed, deliver completion notification.
1714
# It's important to run this *after* resetting the timeout to "rig
1715
# a race" in some test code. writing to the transport will
1716
# synchronously call test code, which synchronously loses the
1717
# connection, calling our connectionLost method, which cancels the
1718
# timeout. We want to make sure that timeout is cancelled *after*
1719
# we reset it above, so that the final state is no timed
1720
# calls. This avoids reactor uncleanliness errors in the test
1722
# XXX: Perhaps loopback should be fixed to not call the user code
1723
# synchronously in transport.write?
1724
self.sendPositiveResponse(tag, 'FETCH completed')
1726
# Instance state is now consistent again (ie, it is as though
1727
# the fetch command never ran), so allow any pending blocked
1728
# commands to execute.
1731
self.spewMessage(id, msg, query, uid
1732
).addCallback(lambda _: self.__cbFetch(results, tag, query, uid)
1733
).addErrback(self.__ebSpewMessage
1736
def __ebSpewMessage(self, failure):
1737
# This indicates a programming error.
1738
# There's no reliable way to indicate anything to the client, since we
1739
# may have already written an arbitrary amount of data in response to
1742
self.transport.loseConnection()
1744
def spew_envelope(self, id, msg, _w=None, _f=None):
1746
_w = self.transport.write
1747
_w('ENVELOPE ' + collapseNestedLists([getEnvelope(msg)]))
1749
def spew_flags(self, id, msg, _w=None, _f=None):
1751
_w = self.transport.write
1752
_w('FLAGS ' + '(%s)' % (' '.join(msg.getFlags())))
1754
def spew_internaldate(self, id, msg, _w=None, _f=None):
1756
_w = self.transport.write
1757
idate = msg.getInternalDate()
1758
ttup = rfc822.parsedate_tz(idate)
1760
log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate))
1761
raise IMAP4Exception("Internal failure generating INTERNALDATE")
1763
odate = time.strftime("%d-%b-%Y %H:%M:%S ", ttup[:9])
1765
odate = odate + "+0000"
1771
odate = odate + sign + string.zfill(str(((abs(ttup[9]) / 3600) * 100 + (abs(ttup[9]) % 3600) / 60)), 4)
1772
_w('INTERNALDATE ' + _quote(odate))
1774
def spew_rfc822header(self, id, msg, _w=None, _f=None):
1776
_w = self.transport.write
1777
hdrs = _formatHeaders(msg.getHeaders(True))
1778
_w('RFC822.HEADER ' + _literal(hdrs))
1780
def spew_rfc822text(self, id, msg, _w=None, _f=None):
1782
_w = self.transport.write
1785
return FileProducer(msg.getBodyFile()
1786
).beginProducing(self.transport
1789
def spew_rfc822size(self, id, msg, _w=None, _f=None):
1791
_w = self.transport.write
1792
_w('RFC822.SIZE ' + str(msg.getSize()))
1794
def spew_rfc822(self, id, msg, _w=None, _f=None):
1796
_w = self.transport.write
1799
mf = IMessageFile(msg, None)
1801
return FileProducer(mf.open()
1802
).beginProducing(self.transport
1804
return MessageProducer(msg, None, self._scheduler
1805
).beginProducing(self.transport
1808
def spew_uid(self, id, msg, _w=None, _f=None):
1810
_w = self.transport.write
1811
_w('UID ' + str(msg.getUID()))
1813
def spew_bodystructure(self, id, msg, _w=None, _f=None):
1814
_w('BODYSTRUCTURE ' + collapseNestedLists([getBodyStructure(msg, True)]))
1816
def spew_body(self, part, id, msg, _w=None, _f=None):
1818
_w = self.transport.write
1820
if msg.isMultipart():
1821
msg = msg.getSubPart(p)
1823
# Non-multipart messages have an implicit first part but no
1824
# other parts - reject any request for any other part.
1825
raise TypeError("Requested subpart of non-multipart message")
1828
hdrs = msg.getHeaders(part.header.negate, *part.header.fields)
1829
hdrs = _formatHeaders(hdrs)
1830
_w(str(part) + ' ' + _literal(hdrs))
1834
return FileProducer(msg.getBodyFile()
1835
).beginProducing(self.transport
1838
hdrs = _formatHeaders(msg.getHeaders(True))
1839
_w(str(part) + ' ' + _literal(hdrs))
1844
return FileProducer(msg.getBodyFile()
1845
).beginProducing(self.transport
1848
mf = IMessageFile(msg, None)
1850
return FileProducer(mf.open()).beginProducing(self.transport)
1851
return MessageProducer(msg, None, self._scheduler).beginProducing(self.transport)
1854
_w('BODY ' + collapseNestedLists([getBodyStructure(msg)]))
1856
def spewMessage(self, id, msg, query, uid):
1857
wbuf = WriteBuffer(self.transport)
1861
write('* %d FETCH (' % (id,))
1871
if part.type == 'uid':
1873
if part.type == 'body':
1874
yield self.spew_body(part, id, msg, write, flush)
1876
f = getattr(self, 'spew_' + part.type)
1877
yield f(id, msg, write, flush)
1878
if part is not query[-1]:
1880
if uid and not seenUID:
1882
yield self.spew_uid(id, msg, write, flush)
1885
return self._scheduler(spew())
1887
def __ebFetch(self, failure, tag):
1888
self.setTimeout(self._oldTimeout)
1889
del self._oldTimeout
1891
self.sendBadResponse(tag, 'FETCH failed: ' + str(failure.value))
1893
def do_STORE(self, tag, messages, mode, flags, uid=0):
1895
silent = mode.endswith('SILENT')
1896
if mode.startswith('+'):
1898
elif mode.startswith('-'):
1903
maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallbacks(
1904
self.__cbStore, self.__ebStore, (tag, self.mbox, uid, silent), None, (tag,), None
1907
select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist)
1909
def __cbStore(self, result, tag, mbox, uid, silent):
1910
if result and not silent:
1911
for (k, v) in result.iteritems():
1913
uidstr = ' UID %d' % mbox.getUID(k)
1916
self.sendUntaggedResponse('%d FETCH (FLAGS (%s)%s)' %
1917
(k, ' '.join(v), uidstr))
1918
self.sendPositiveResponse(tag, 'STORE completed')
1920
def __ebStore(self, failure, tag):
1921
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1923
def do_COPY(self, tag, messages, mailbox, uid=0):
1924
mailbox = self._parseMbox(mailbox)
1925
maybeDeferred(self.account.select, mailbox
1926
).addCallback(self._cbCopySelectedMailbox, tag, messages, mailbox, uid
1927
).addErrback(self._ebCopySelectedMailbox, tag
1929
select_COPY = (do_COPY, arg_seqset, arg_astring)
1931
def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid):
1933
self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox)
1935
maybeDeferred(self.mbox.fetch, messages, uid
1936
).addCallback(self.__cbCopy, tag, mbox
1937
).addCallback(self.__cbCopied, tag, mbox
1938
).addErrback(self.__ebCopy, tag
1941
def _ebCopySelectedMailbox(self, failure, tag):
1942
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1944
def __cbCopy(self, messages, tag, mbox):
1945
# XXX - This should handle failures with a rollback or something
1950
fastCopyMbox = IMessageCopier(mbox, None)
1951
for (id, msg) in messages:
1952
if fastCopyMbox is not None:
1953
d = maybeDeferred(fastCopyMbox.copy, msg)
1954
addedDeferreds.append(d)
1957
# XXX - The following should be an implementation of IMessageCopier.copy
1958
# on an IMailbox->IMessageCopier adapter.
1960
flags = msg.getFlags()
1961
date = msg.getInternalDate()
1963
body = IMessageFile(msg, None)
1964
if body is not None:
1965
bodyFile = body.open()
1966
d = maybeDeferred(mbox.addMessage, bodyFile, flags, date)
1971
buffer = tempfile.TemporaryFile()
1972
d = MessageProducer(msg, buffer, self._scheduler
1973
).beginProducing(None
1974
).addCallback(lambda _, b=buffer, f=flags, d=date: mbox.addMessage(rewind(b), f, d)
1976
addedDeferreds.append(d)
1977
return defer.DeferredList(addedDeferreds)
1979
def __cbCopied(self, deferredIds, tag, mbox):
1982
for (status, result) in deferredIds:
1986
failures.append(result.value)
1988
self.sendNegativeResponse(tag, '[ALERT] Some messages were not copied')
1990
self.sendPositiveResponse(tag, 'COPY completed')
1992
def __ebCopy(self, failure, tag):
1993
self.sendBadResponse(tag, 'COPY failed:' + str(failure.value))
1996
def do_UID(self, tag, command, line):
1997
command = command.upper()
1999
if command not in ('COPY', 'FETCH', 'STORE', 'SEARCH'):
2000
raise IllegalClientResponse(command)
2002
self.dispatchCommand(tag, command, line, uid=1)
2004
select_UID = (do_UID, arg_atom, arg_line)
2006
# IMailboxListener implementation
2008
def modeChanged(self, writeable):
2010
self.sendUntaggedResponse(message='[READ-WRITE]', async=True)
2012
self.sendUntaggedResponse(message='[READ-ONLY]', async=True)
2014
def flagsChanged(self, newFlags):
2015
for (mId, flags) in newFlags.iteritems():
2016
msg = '%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags))
2017
self.sendUntaggedResponse(msg, async=True)
2019
def newMessages(self, exists, recent):
2020
if exists is not None:
2021
self.sendUntaggedResponse('%d EXISTS' % exists, async=True)
2022
if recent is not None:
2023
self.sendUntaggedResponse('%d RECENT' % recent, async=True)
2026
class UnhandledResponse(IMAP4Exception): pass
2028
class NegativeResponse(IMAP4Exception): pass
2030
class NoSupportedAuthentication(IMAP4Exception):
2031
def __init__(self, serverSupports, clientSupports):
2032
IMAP4Exception.__init__(self, 'No supported authentication schemes available')
2033
self.serverSupports = serverSupports
2034
self.clientSupports = clientSupports
2037
return (IMAP4Exception.__str__(self)
2038
+ ': Server supports %r, client supports %r'
2039
% (self.serverSupports, self.clientSupports))
2041
class IllegalServerResponse(IMAP4Exception): pass
2043
TIMEOUT_ERROR = error.TimeoutError()
2045
class IMAP4Client(basic.LineReceiver, policies.TimeoutMixin):
2046
"""IMAP4 client protocol implementation
2048
@ivar state: A string representing the state the connection is currently
2051
implements(IMailboxListener)
2061
# Number of seconds to wait before timing out a connection.
2062
# If the number is <= 0 no timeout checking will be performed.
2065
# Capabilities are not allowed to change during the session
2066
# So cache the first response and use that for all later
2070
_memoryFileLimit = 1024 * 1024 * 10
2072
# Authentication is pluggable. This maps names to IClientAuthentication
2074
authenticators = None
2076
STATUS_CODES = ('OK', 'NO', 'BAD', 'PREAUTH', 'BYE')
2078
STATUS_TRANSFORMATIONS = {
2079
'MESSAGES': int, 'RECENT': int, 'UNSEEN': int
2084
def __init__(self, contextFactory = None):
2087
self.authenticators = {}
2088
self.context = contextFactory
2092
self._lastCmd = None
2094
def registerAuthenticator(self, auth):
2095
"""Register a new form of authentication
2097
When invoking the authenticate() method of IMAP4Client, the first
2098
matching authentication scheme found will be used. The ordering is
2099
that in which the server lists support authentication schemes.
2101
@type auth: Implementor of C{IClientAuthentication}
2102
@param auth: The object to use to perform the client
2103
side of this authentication scheme.
2105
self.authenticators[auth.getName().upper()] = auth
2107
def rawDataReceived(self, data):
2108
if self.timeout > 0:
2111
self._pendingSize -= len(data)
2112
if self._pendingSize > 0:
2113
self._pendingBuffer.write(data)
2116
if self._pendingSize < 0:
2117
data, passon = data[:self._pendingSize], data[self._pendingSize:]
2118
self._pendingBuffer.write(data)
2119
rest = self._pendingBuffer
2120
self._pendingBuffer = None
2121
self._pendingSize = None
2123
self._parts.append(rest.read())
2124
self.setLineMode(passon.lstrip('\r\n'))
2126
# def sendLine(self, line):
2127
# print 'S:', repr(line)
2128
# return basic.LineReceiver.sendLine(self, line)
2130
def _setupForLiteral(self, rest, octets):
2131
self._pendingBuffer = self.messageFile(octets)
2132
self._pendingSize = octets
2133
if self._parts is None:
2134
self._parts = [rest, '\r\n']
2136
self._parts.extend([rest, '\r\n'])
2139
def connectionMade(self):
2140
if self.timeout > 0:
2141
self.setTimeout(self.timeout)
2143
def connectionLost(self, reason):
2144
"""We are no longer connected"""
2145
if self.timeout > 0:
2146
self.setTimeout(None)
2147
if self.queued is not None:
2148
queued = self.queued
2151
cmd.defer.errback(reason)
2152
if self.tags is not None:
2155
for cmd in tags.itervalues():
2156
if cmd is not None and cmd.defer is not None:
2157
cmd.defer.errback(reason)
2160
def lineReceived(self, line):
2162
Attempt to parse a single line from the server.
2165
@param line: The line from the server, without the line delimiter.
2167
@raise IllegalServerResponse: If the line or some part of the line
2168
does not represent an allowed message from the server at this time.
2170
# print 'C: ' + repr(line)
2171
if self.timeout > 0:
2174
lastPart = line.rfind('{')
2176
lastPart = line[lastPart + 1:]
2177
if lastPart.endswith('}'):
2178
# It's a literal a-comin' in
2180
octets = int(lastPart[:-1])
2182
raise IllegalServerResponse(line)
2183
if self._parts is None:
2184
self._tag, parts = line.split(None, 1)
2187
self._setupForLiteral(parts, octets)
2190
if self._parts is None:
2191
# It isn't a literal at all
2192
self._regularDispatch(line)
2194
# If an expression is in progress, no tag is required here
2195
# Since we didn't find a literal indicator, this expression
2197
self._parts.append(line)
2198
tag, rest = self._tag, ''.join(self._parts)
2199
self._tag = self._parts = None
2200
self.dispatchCommand(tag, rest)
2202
def timeoutConnection(self):
2203
if self._lastCmd and self._lastCmd.defer is not None:
2204
d, self._lastCmd.defer = self._lastCmd.defer, None
2205
d.errback(TIMEOUT_ERROR)
2208
for cmd in self.queued:
2209
if cmd.defer is not None:
2210
d, cmd.defer = cmd.defer, d
2211
d.errback(TIMEOUT_ERROR)
2213
self.transport.loseConnection()
2215
def _regularDispatch(self, line):
2216
parts = line.split(None, 1)
2220
self.dispatchCommand(tag, rest)
2222
def messageFile(self, octets):
2223
"""Create a file to which an incoming message may be written.
2225
@type octets: C{int}
2226
@param octets: The number of octets which will be written to the file
2228
@rtype: Any object which implements C{write(string)} and
2230
@return: A file-like object
2232
if octets > self._memoryFileLimit:
2233
return tempfile.TemporaryFile()
2235
return StringIO.StringIO()
2238
tag = '%0.4X' % self.tagID
2242
def dispatchCommand(self, tag, rest):
2243
if self.state is None:
2244
f = self.response_UNAUTH
2246
f = getattr(self, 'response_' + self.state.upper(), None)
2252
self.transport.loseConnection()
2254
log.err("Cannot dispatch: %s, %s, %s" % (self.state, tag, rest))
2255
self.transport.loseConnection()
2257
def response_UNAUTH(self, tag, rest):
2258
if self.state is None:
2259
# Server greeting, this is
2260
status, rest = rest.split(None, 1)
2261
if status.upper() == 'OK':
2262
self.state = 'unauth'
2263
elif status.upper() == 'PREAUTH':
2266
# XXX - This is rude.
2267
self.transport.loseConnection()
2268
raise IllegalServerResponse(tag + ' ' + rest)
2270
b, e = rest.find('['), rest.find(']')
2271
if b != -1 and e != -1:
2272
self.serverGreeting(
2273
self.__cbCapabilities(
2274
([parseNestedParens(rest[b + 1:e])], None)))
2276
self.serverGreeting(None)
2278
self._defaultHandler(tag, rest)
2280
def response_AUTH(self, tag, rest):
2281
self._defaultHandler(tag, rest)
2283
def _defaultHandler(self, tag, rest):
2284
if tag == '*' or tag == '+':
2285
if not self.waiting:
2286
self._extraInfo([parseNestedParens(rest)])
2288
cmd = self.tags[self.waiting]
2290
cmd.continuation(rest)
2292
cmd.lines.append(rest)
2295
cmd = self.tags[tag]
2297
# XXX - This is rude.
2298
self.transport.loseConnection()
2299
raise IllegalServerResponse(tag + ' ' + rest)
2301
status, line = rest.split(None, 1)
2303
# Give them this last line, too
2304
cmd.finish(rest, self._extraInfo)
2306
cmd.defer.errback(IMAP4Exception(line))
2311
def _flushQueue(self):
2313
cmd = self.queued.pop(0)
2316
self.sendLine(cmd.format(t))
2319
def _extraInfo(self, lines):
2320
# XXX - This is terrible.
2321
# XXX - Also, this should collapse temporally proximate calls into single
2322
# invocations of IMailboxListener methods, where possible.
2324
recent = exists = None
2325
for response in lines:
2326
elements = len(response)
2327
if elements == 1 and response[0] == ['READ-ONLY']:
2328
self.modeChanged(False)
2329
elif elements == 1 and response[0] == ['READ-WRITE']:
2330
self.modeChanged(True)
2331
elif elements == 2 and response[1] == 'EXISTS':
2332
exists = int(response[0])
2333
elif elements == 2 and response[1] == 'RECENT':
2334
recent = int(response[0])
2335
elif elements == 3 and response[1] == 'FETCH':
2336
mId = int(response[0])
2337
values = self._parseFetchPairs(response[2])
2338
flags.setdefault(mId, []).extend(values.get('FLAGS', ()))
2340
log.msg('Unhandled unsolicited response: %s' % (response,))
2343
self.flagsChanged(flags)
2344
if recent is not None or exists is not None:
2345
self.newMessages(exists, recent)
2347
def sendCommand(self, cmd):
2348
cmd.defer = defer.Deferred()
2350
self.queued.append(cmd)
2354
self.sendLine(cmd.format(t))
2359
def getCapabilities(self, useCache=1):
2360
"""Request the capabilities available on this server.
2362
This command is allowed in any state of connection.
2364
@type useCache: C{bool}
2365
@param useCache: Specify whether to use the capability-cache or to
2366
re-retrieve the capabilities from the server. Server capabilities
2367
should never change, so for normal use, this flag should never be
2371
@return: A deferred whose callback will be invoked with a
2372
dictionary mapping capability types to lists of supported
2373
mechanisms, or to None if a support list is not applicable.
2375
if useCache and self._capCache is not None:
2376
return defer.succeed(self._capCache)
2378
resp = ('CAPABILITY',)
2379
d = self.sendCommand(Command(cmd, wantResponse=resp))
2380
d.addCallback(self.__cbCapabilities)
2383
def __cbCapabilities(self, (lines, tagline)):
2386
for cap in rest[1:]:
2387
parts = cap.split('=', 1)
2389
category, value = parts[0], None
2391
category, value = parts
2392
caps.setdefault(category, []).append(value)
2394
# Preserve a non-ideal API for backwards compatibility. It would
2395
# probably be entirely sensible to have an object with a wider API than
2396
# dict here so this could be presented less insanely.
2397
for category in caps:
2398
if caps[category] == [None]:
2399
caps[category] = None
2400
self._capCache = caps
2404
"""Inform the server that we are done with the connection.
2406
This command is allowed in any state of connection.
2409
@return: A deferred whose callback will be invoked with None
2410
when the proper server acknowledgement has been received.
2412
d = self.sendCommand(Command('LOGOUT', wantResponse=('BYE',)))
2413
d.addCallback(self.__cbLogout)
2416
def __cbLogout(self, (lines, tagline)):
2417
self.transport.loseConnection()
2418
# We don't particularly care what the server said
2423
"""Perform no operation.
2425
This command is allowed in any state of connection.
2428
@return: A deferred whose callback will be invoked with a list
2429
of untagged status updates the server responds with.
2431
d = self.sendCommand(Command('NOOP'))
2432
d.addCallback(self.__cbNoop)
2435
def __cbNoop(self, (lines, tagline)):
2436
# Conceivable, this is elidable.
2437
# It is, afterall, a no-op.
2440
def startTLS(self, contextFactory=None):
2442
Initiates a 'STARTTLS' request and negotiates the TLS / SSL
2445
@param contextFactory: The TLS / SSL Context Factory to
2446
leverage. If the contextFactory is None the IMAP4Client will
2447
either use the current TLS / SSL Context Factory or attempt to
2450
@type contextFactory: C{ssl.ClientContextFactory}
2452
@return: A Deferred which fires when the transport has been
2453
secured according to the given contextFactory, or which fails
2454
if the transport cannot be secured.
2456
assert not self.startedTLS, "Client and Server are currently communicating via TLS"
2458
if contextFactory is None:
2459
contextFactory = self._getContextFactory()
2461
if contextFactory is None:
2462
return defer.fail(IMAP4Exception(
2463
"IMAP4Client requires a TLS context to "
2464
"initiate the STARTTLS handshake"))
2466
if 'STARTTLS' not in self._capCache:
2467
return defer.fail(IMAP4Exception(
2468
"Server does not support secure communication "
2471
tls = interfaces.ITLSTransport(self.transport, None)
2473
return defer.fail(IMAP4Exception(
2474
"IMAP4Client transport does not implement "
2475
"interfaces.ITLSTransport"))
2477
d = self.sendCommand(Command('STARTTLS'))
2478
d.addCallback(self._startedTLS, contextFactory)
2479
d.addCallback(lambda _: self.getCapabilities())
2483
def authenticate(self, secret):
2484
"""Attempt to enter the authenticated state with the server
2486
This command is allowed in the Non-Authenticated state.
2489
@return: A deferred whose callback is invoked if the authentication
2490
succeeds and whose errback will be invoked otherwise.
2492
if self._capCache is None:
2493
d = self.getCapabilities()
2495
d = defer.succeed(self._capCache)
2496
d.addCallback(self.__cbAuthenticate, secret)
2499
def __cbAuthenticate(self, caps, secret):
2500
auths = caps.get('AUTH', ())
2501
for scheme in auths:
2502
if scheme.upper() in self.authenticators:
2503
cmd = Command('AUTHENTICATE', scheme, (),
2504
self.__cbContinueAuth, scheme,
2506
return self.sendCommand(cmd)
2509
return defer.fail(NoSupportedAuthentication(
2510
auths, self.authenticators.keys()))
2512
def ebStartTLS(err):
2513
err.trap(IMAP4Exception)
2514
# We couldn't negotiate TLS for some reason
2515
return defer.fail(NoSupportedAuthentication(
2516
auths, self.authenticators.keys()))
2519
d.addErrback(ebStartTLS)
2520
d.addCallback(lambda _: self.getCapabilities())
2521
d.addCallback(self.__cbAuthTLS, secret)
2525
def __cbContinueAuth(self, rest, scheme, secret):
2527
chal = base64.decodestring(rest + '\n')
2528
except binascii.Error:
2530
raise IllegalServerResponse(rest)
2531
self.transport.loseConnection()
2533
auth = self.authenticators[scheme]
2534
chal = auth.challengeResponse(secret, chal)
2535
self.sendLine(base64.encodestring(chal).strip())
2537
def __cbAuthTLS(self, caps, secret):
2538
auths = caps.get('AUTH', ())
2539
for scheme in auths:
2540
if scheme.upper() in self.authenticators:
2541
cmd = Command('AUTHENTICATE', scheme, (),
2542
self.__cbContinueAuth, scheme,
2544
return self.sendCommand(cmd)
2545
raise NoSupportedAuthentication(auths, self.authenticators.keys())
2548
def login(self, username, password):
2549
"""Authenticate with the server using a username and password
2551
This command is allowed in the Non-Authenticated state. If the
2552
server supports the STARTTLS capability and our transport supports
2553
TLS, TLS is negotiated before the login command is issued.
2555
A more secure way to log in is to use C{startTLS} or
2556
C{authenticate} or both.
2558
@type username: C{str}
2559
@param username: The username to log in with
2561
@type password: C{str}
2562
@param password: The password to log in with
2565
@return: A deferred whose callback is invoked if login is successful
2566
and whose errback is invoked otherwise.
2568
d = maybeDeferred(self.getCapabilities)
2569
d.addCallback(self.__cbLoginCaps, username, password)
2572
def serverGreeting(self, caps):
2573
"""Called when the server has sent us a greeting.
2576
@param caps: Capabilities the server advertised in its greeting.
2579
def _getContextFactory(self):
2580
if self.context is not None:
2583
from twisted.internet import ssl
2587
context = ssl.ClientContextFactory()
2588
context.method = ssl.SSL.TLSv1_METHOD
2591
def __cbLoginCaps(self, capabilities, username, password):
2592
# If the server advertises STARTTLS, we might want to try to switch to TLS
2593
tryTLS = 'STARTTLS' in capabilities
2595
# If our transport supports switching to TLS, we might want to try to switch to TLS.
2596
tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None
2598
# If our transport is not already using TLS, we might want to try to switch to TLS.
2599
nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None
2601
if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport:
2607
callbackArgs=(username, password),
2612
log.msg("Server has no TLS support. logging in over cleartext!")
2613
args = ' '.join((_quote(username), _quote(password)))
2614
return self.sendCommand(Command('LOGIN', args))
2616
def _startedTLS(self, result, context):
2617
self.transport.startTLS(context)
2618
self._capCache = None
2619
self.startedTLS = True
2622
def __cbLoginTLS(self, result, username, password):
2623
args = ' '.join((_quote(username), _quote(password)))
2624
return self.sendCommand(Command('LOGIN', args))
2626
def __ebLoginTLS(self, failure):
2630
def namespace(self):
2631
"""Retrieve information about the namespaces available to this account
2633
This command is allowed in the Authenticated and Selected states.
2636
@return: A deferred whose callback is invoked with namespace
2637
information. An example of this information is::
2639
[[['', '/']], [], []]
2641
which indicates a single personal namespace called '' with '/'
2642
as its hierarchical delimiter, and no shared or user namespaces.
2645
resp = ('NAMESPACE',)
2646
d = self.sendCommand(Command(cmd, wantResponse=resp))
2647
d.addCallback(self.__cbNamespace)
2650
def __cbNamespace(self, (lines, last)):
2652
if len(parts) == 4 and parts[0] == 'NAMESPACE':
2653
return [e or [] for e in parts[1:]]
2654
log.err("No NAMESPACE response to NAMESPACE command")
2657
def select(self, mailbox):
2660
This command is allowed in the Authenticated and Selected states.
2662
@type mailbox: C{str}
2663
@param mailbox: The name of the mailbox to select
2666
@return: A deferred whose callback is invoked with mailbox
2667
information if the select is successful and whose errback is
2668
invoked otherwise. Mailbox information consists of a dictionary
2669
with the following keys and values::
2671
FLAGS: A list of strings containing the flags settable on
2672
messages in this mailbox.
2674
EXISTS: An integer indicating the number of messages in this
2677
RECENT: An integer indicating the number of \"recent\"
2678
messages in this mailbox.
2680
UNSEEN: An integer indicating the number of messages not
2681
flagged \\Seen in this mailbox.
2683
PERMANENTFLAGS: A list of strings containing the flags that
2684
can be permanently set on messages in this mailbox.
2686
UIDVALIDITY: An integer uniquely identifying this mailbox.
2689
args = _prepareMailboxName(mailbox)
2690
resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
2691
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2692
d.addCallback(self.__cbSelect, 1)
2695
def examine(self, mailbox):
2696
"""Select a mailbox in read-only mode
2698
This command is allowed in the Authenticated and Selected states.
2700
@type mailbox: C{str}
2701
@param mailbox: The name of the mailbox to examine
2704
@return: A deferred whose callback is invoked with mailbox
2705
information if the examine is successful and whose errback
2706
is invoked otherwise. Mailbox information consists of a dictionary
2707
with the following keys and values::
2709
'FLAGS': A list of strings containing the flags settable on
2710
messages in this mailbox.
2712
'EXISTS': An integer indicating the number of messages in this
2715
'RECENT': An integer indicating the number of \"recent\"
2716
messages in this mailbox.
2718
'UNSEEN': An integer indicating the number of messages not
2719
flagged \\Seen in this mailbox.
2721
'PERMANENTFLAGS': A list of strings containing the flags that
2722
can be permanently set on messages in this mailbox.
2724
'UIDVALIDITY': An integer uniquely identifying this mailbox.
2727
args = _prepareMailboxName(mailbox)
2728
resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
2729
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2730
d.addCallback(self.__cbSelect, 0)
2734
def _intOrRaise(self, value, phrase):
2736
Parse C{value} as an integer and return the result or raise
2737
L{IllegalServerResponse} with C{phrase} as an argument if C{value}
2738
cannot be parsed as an integer.
2743
raise IllegalServerResponse(phrase)
2746
def __cbSelect(self, (lines, tagline), rw):
2748
Handle lines received in response to a SELECT or EXAMINE command.
2750
See RFC 3501, section 6.3.1.
2752
# In the absense of specification, we are free to assume:
2754
datum = {'READ-WRITE': rw}
2755
lines.append(parseNestedParens(tagline))
2757
if len(split) > 0 and split[0].upper() == 'OK':
2758
# Handle all the kinds of OK response.
2760
key = content[0].upper()
2761
if key == 'READ-ONLY':
2762
datum['READ-WRITE'] = False
2763
elif key == 'READ-WRITE':
2764
datum['READ-WRITE'] = True
2765
elif key == 'UIDVALIDITY':
2766
datum['UIDVALIDITY'] = self._intOrRaise(
2768
elif key == 'UNSEEN':
2769
datum['UNSEEN'] = self._intOrRaise(content[1], split)
2770
elif key == 'UIDNEXT':
2771
datum['UIDNEXT'] = self._intOrRaise(content[1], split)
2772
elif key == 'PERMANENTFLAGS':
2773
datum['PERMANENTFLAGS'] = tuple(content[1])
2775
log.err('Unhandled SELECT response (2): %s' % (split,))
2776
elif len(split) == 2:
2777
# Handle FLAGS, EXISTS, and RECENT
2778
if split[0].upper() == 'FLAGS':
2779
datum['FLAGS'] = tuple(split[1])
2780
elif isinstance(split[1], str):
2781
# Must make sure things are strings before treating them as
2782
# strings since some other forms of response have nesting in
2783
# places which results in lists instead.
2784
if split[1].upper() == 'EXISTS':
2785
datum['EXISTS'] = self._intOrRaise(split[0], split)
2786
elif split[1].upper() == 'RECENT':
2787
datum['RECENT'] = self._intOrRaise(split[0], split)
2789
log.err('Unhandled SELECT response (0): %s' % (split,))
2791
log.err('Unhandled SELECT response (1): %s' % (split,))
2793
log.err('Unhandled SELECT response (4): %s' % (split,))
2797
def create(self, name):
2798
"""Create a new mailbox on the server
2800
This command is allowed in the Authenticated and Selected states.
2803
@param name: The name of the mailbox to create.
2806
@return: A deferred whose callback is invoked if the mailbox creation
2807
is successful and whose errback is invoked otherwise.
2809
return self.sendCommand(Command('CREATE', _prepareMailboxName(name)))
2811
def delete(self, name):
2814
This command is allowed in the Authenticated and Selected states.
2817
@param name: The name of the mailbox to delete.
2820
@return: A deferred whose calblack is invoked if the mailbox is
2821
deleted successfully and whose errback is invoked otherwise.
2823
return self.sendCommand(Command('DELETE', _prepareMailboxName(name)))
2825
def rename(self, oldname, newname):
2828
This command is allowed in the Authenticated and Selected states.
2830
@type oldname: C{str}
2831
@param oldname: The current name of the mailbox to rename.
2833
@type newname: C{str}
2834
@param newname: The new name to give the mailbox.
2837
@return: A deferred whose callback is invoked if the rename is
2838
successful and whose errback is invoked otherwise.
2840
oldname = _prepareMailboxName(oldname)
2841
newname = _prepareMailboxName(newname)
2842
return self.sendCommand(Command('RENAME', ' '.join((oldname, newname))))
2844
def subscribe(self, name):
2845
"""Add a mailbox to the subscription list
2847
This command is allowed in the Authenticated and Selected states.
2850
@param name: The mailbox to mark as 'active' or 'subscribed'
2853
@return: A deferred whose callback is invoked if the subscription
2854
is successful and whose errback is invoked otherwise.
2856
return self.sendCommand(Command('SUBSCRIBE', _prepareMailboxName(name)))
2858
def unsubscribe(self, name):
2859
"""Remove a mailbox from the subscription list
2861
This command is allowed in the Authenticated and Selected states.
2864
@param name: The mailbox to unsubscribe
2867
@return: A deferred whose callback is invoked if the unsubscription
2868
is successful and whose errback is invoked otherwise.
2870
return self.sendCommand(Command('UNSUBSCRIBE', _prepareMailboxName(name)))
2872
def list(self, reference, wildcard):
2873
"""List a subset of the available mailboxes
2875
This command is allowed in the Authenticated and Selected states.
2877
@type reference: C{str}
2878
@param reference: The context in which to interpret C{wildcard}
2880
@type wildcard: C{str}
2881
@param wildcard: The pattern of mailbox names to match, optionally
2882
including either or both of the '*' and '%' wildcards. '*' will
2883
match zero or more characters and cross hierarchical boundaries.
2884
'%' will also match zero or more characters, but is limited to a
2885
single hierarchical level.
2888
@return: A deferred whose callback is invoked with a list of C{tuple}s,
2889
the first element of which is a C{tuple} of mailbox flags, the second
2890
element of which is the hierarchy delimiter for this mailbox, and the
2891
third of which is the mailbox name; if the command is unsuccessful,
2892
the deferred's errback is invoked instead.
2895
args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
2897
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2898
d.addCallback(self.__cbList, 'LIST')
2901
def lsub(self, reference, wildcard):
2902
"""List a subset of the subscribed available mailboxes
2904
This command is allowed in the Authenticated and Selected states.
2906
The parameters and returned object are the same as for the C{list}
2907
method, with one slight difference: Only mailboxes which have been
2908
subscribed can be included in the resulting list.
2911
args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
2913
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2914
d.addCallback(self.__cbList, 'LSUB')
2917
def __cbList(self, (lines, last), command):
2920
if len(parts) == 4 and parts[0] == command:
2921
parts[1] = tuple(parts[1])
2922
results.append(tuple(parts[1:]))
2925
def status(self, mailbox, *names):
2927
Retrieve the status of the given mailbox
2929
This command is allowed in the Authenticated and Selected states.
2931
@type mailbox: C{str}
2932
@param mailbox: The name of the mailbox to query
2934
@type *names: C{str}
2935
@param *names: The status names to query. These may be any number of:
2936
C{'MESSAGES'}, C{'RECENT'}, C{'UIDNEXT'}, C{'UIDVALIDITY'}, and
2940
@return: A deferred which fires with with the status information if the
2941
command is successful and whose errback is invoked otherwise. The
2942
status information is in the form of a C{dict}. Each element of
2943
C{names} is a key in the dictionary. The value for each key is the
2944
corresponding response from the server.
2947
args = "%s (%s)" % (_prepareMailboxName(mailbox), ' '.join(names))
2949
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2950
d.addCallback(self.__cbStatus)
2953
def __cbStatus(self, (lines, last)):
2956
if parts[0] == 'STATUS':
2958
items = [items[i:i+2] for i in range(0, len(items), 2)]
2959
status.update(dict(items))
2960
for k in status.keys():
2961
t = self.STATUS_TRANSFORMATIONS.get(k)
2964
status[k] = t(status[k])
2965
except Exception, e:
2966
raise IllegalServerResponse('(%s %s): %s' % (k, status[k], str(e)))
2969
def append(self, mailbox, message, flags = (), date = None):
2970
"""Add the given message to the given mailbox.
2972
This command is allowed in the Authenticated and Selected states.
2974
@type mailbox: C{str}
2975
@param mailbox: The mailbox to which to add this message.
2977
@type message: Any file-like object
2978
@param message: The message to add, in RFC822 format. Newlines
2979
in this file should be \\r\\n-style.
2981
@type flags: Any iterable of C{str}
2982
@param flags: The flags to associated with this message.
2985
@param date: The date to associate with this message. This should
2986
be of the format DD-MM-YYYY HH:MM:SS +/-HHMM. For example, in
2987
Eastern Standard Time, on July 1st 2004 at half past 1 PM,
2988
\"01-07-2004 13:30:00 -0500\".
2991
@return: A deferred whose callback is invoked when this command
2992
succeeds or whose errback is invoked if it fails.
2997
fmt = '%s (%s)%s {%d}'
2999
date = ' "%s"' % date
3003
_prepareMailboxName(mailbox), ' '.join(flags),
3006
d = self.sendCommand(Command('APPEND', cmd, (), self.__cbContinueAppend, message))
3009
def __cbContinueAppend(self, lines, message):
3010
s = basic.FileSender()
3011
return s.beginFileTransfer(message, self.transport, None
3012
).addCallback(self.__cbFinishAppend)
3014
def __cbFinishAppend(self, foo):
3018
"""Tell the server to perform a checkpoint
3020
This command is allowed in the Selected state.
3023
@return: A deferred whose callback is invoked when this command
3024
succeeds or whose errback is invoked if it fails.
3026
return self.sendCommand(Command('CHECK'))
3029
"""Return the connection to the Authenticated state.
3031
This command is allowed in the Selected state.
3033
Issuing this command will also remove all messages flagged \\Deleted
3034
from the selected mailbox if it is opened in read-write mode,
3035
otherwise it indicates success by no messages are removed.
3038
@return: A deferred whose callback is invoked when the command
3039
completes successfully or whose errback is invoked if it fails.
3041
return self.sendCommand(Command('CLOSE'))
3045
"""Return the connection to the Authenticate state.
3047
This command is allowed in the Selected state.
3049
Issuing this command will perform the same actions as issuing the
3050
close command, but will also generate an 'expunge' response for
3051
every message deleted.
3054
@return: A deferred whose callback is invoked with a list of the
3055
'expunge' responses when this command is successful or whose errback
3056
is invoked otherwise.
3060
d = self.sendCommand(Command(cmd, wantResponse=resp))
3061
d.addCallback(self.__cbExpunge)
3065
def __cbExpunge(self, (lines, last)):
3068
if len(parts) == 2 and parts[1] == 'EXPUNGE':
3069
ids.append(self._intOrRaise(parts[0], parts))
3073
def search(self, *queries, **kwarg):
3074
"""Search messages in the currently selected mailbox
3076
This command is allowed in the Selected state.
3078
Any non-zero number of queries are accepted by this method, as
3079
returned by the C{Query}, C{Or}, and C{Not} functions.
3081
One keyword argument is accepted: if uid is passed in with a non-zero
3082
value, the server is asked to return message UIDs instead of message
3086
@return: A deferred whose callback will be invoked with a list of all
3087
the message sequence numbers return by the search, or whose errback
3088
will be invoked if there is an error.
3090
if kwarg.get('uid'):
3094
args = ' '.join(queries)
3095
d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,)))
3096
d.addCallback(self.__cbSearch)
3100
def __cbSearch(self, (lines, end)):
3103
if len(parts) > 0 and parts[0] == 'SEARCH':
3104
ids.extend([self._intOrRaise(p, parts) for p in parts[1:]])
3108
def fetchUID(self, messages, uid=0):
3109
"""Retrieve the unique identifier for one or more messages
3111
This command is allowed in the Selected state.
3113
@type messages: C{MessageSet} or C{str}
3114
@param messages: A message sequence set
3117
@param uid: Indicates whether the message sequence set is of message
3118
numbers or of unique message IDs.
3121
@return: A deferred whose callback is invoked with a dict mapping
3122
message sequence numbers to unique message identifiers, or whose
3123
errback is invoked if there is an error.
3125
return self._fetch(messages, useUID=uid, uid=1)
3128
def fetchFlags(self, messages, uid=0):
3129
"""Retrieve the flags for one or more messages
3131
This command is allowed in the Selected state.
3133
@type messages: C{MessageSet} or C{str}
3134
@param messages: The messages for which to retrieve flags.
3137
@param uid: Indicates whether the message sequence set is of message
3138
numbers or of unique message IDs.
3141
@return: A deferred whose callback is invoked with a dict mapping
3142
message numbers to lists of flags, or whose errback is invoked if
3145
return self._fetch(str(messages), useUID=uid, flags=1)
3148
def fetchInternalDate(self, messages, uid=0):
3149
"""Retrieve the internal date associated with one or more messages
3151
This command is allowed in the Selected state.
3153
@type messages: C{MessageSet} or C{str}
3154
@param messages: The messages for which to retrieve the internal date.
3157
@param uid: Indicates whether the message sequence set is of message
3158
numbers or of unique message IDs.
3161
@return: A deferred whose callback is invoked with a dict mapping
3162
message numbers to date strings, or whose errback is invoked
3163
if there is an error. Date strings take the format of
3164
\"day-month-year time timezone\".
3166
return self._fetch(str(messages), useUID=uid, internaldate=1)
3169
def fetchEnvelope(self, messages, uid=0):
3170
"""Retrieve the envelope data for one or more messages
3172
This command is allowed in the Selected state.
3174
@type messages: C{MessageSet} or C{str}
3175
@param messages: The messages for which to retrieve envelope data.
3178
@param uid: Indicates whether the message sequence set is of message
3179
numbers or of unique message IDs.
3182
@return: A deferred whose callback is invoked with a dict mapping
3183
message numbers to envelope data, or whose errback is invoked
3184
if there is an error. Envelope data consists of a sequence of the
3185
date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
3186
and message-id header fields. The date, subject, in-reply-to, and
3187
message-id fields are strings, while the from, sender, reply-to,
3188
to, cc, and bcc fields contain address data. Address data consists
3189
of a sequence of name, source route, mailbox name, and hostname.
3190
Fields which are not present for a particular address may be C{None}.
3192
return self._fetch(str(messages), useUID=uid, envelope=1)
3195
def fetchBodyStructure(self, messages, uid=0):
3196
"""Retrieve the structure of the body of one or more messages
3198
This command is allowed in the Selected state.
3200
@type messages: C{MessageSet} or C{str}
3201
@param messages: The messages for which to retrieve body structure
3205
@param uid: Indicates whether the message sequence set is of message
3206
numbers or of unique message IDs.
3209
@return: A deferred whose callback is invoked with a dict mapping
3210
message numbers to body structure data, or whose errback is invoked
3211
if there is an error. Body structure data describes the MIME-IMB
3212
format of a message and consists of a sequence of mime type, mime
3213
subtype, parameters, content id, description, encoding, and size.
3214
The fields following the size field are variable: if the mime
3215
type/subtype is message/rfc822, the contained message's envelope
3216
information, body structure data, and number of lines of text; if
3217
the mime type is text, the number of lines of text. Extension fields
3218
may also be included; if present, they are: the MD5 hash of the body,
3219
body disposition, body language.
3221
return self._fetch(messages, useUID=uid, bodystructure=1)
3224
def fetchSimplifiedBody(self, messages, uid=0):
3225
"""Retrieve the simplified body structure of one or more messages
3227
This command is allowed in the Selected state.
3229
@type messages: C{MessageSet} or C{str}
3230
@param messages: A message sequence set
3233
@param uid: Indicates whether the message sequence set is of message
3234
numbers or of unique message IDs.
3237
@return: A deferred whose callback is invoked with a dict mapping
3238
message numbers to body data, or whose errback is invoked
3239
if there is an error. The simplified body structure is the same
3240
as the body structure, except that extension fields will never be
3243
return self._fetch(messages, useUID=uid, body=1)
3246
def fetchMessage(self, messages, uid=0):
3247
"""Retrieve one or more entire messages
3249
This command is allowed in the Selected state.
3251
@type messages: L{MessageSet} or C{str}
3252
@param messages: A message sequence set
3255
@param uid: Indicates whether the message sequence set is of message
3256
numbers or of unique message IDs.
3260
@return: A L{Deferred} which will fire with a C{dict} mapping message
3261
sequence numbers to C{dict}s giving message data for the
3262
corresponding message. If C{uid} is true, the inner dictionaries
3263
have a C{'UID'} key mapped to a C{str} giving the UID for the
3264
message. The text of the message is a C{str} associated with the
3265
C{'RFC822'} key in each dictionary.
3267
return self._fetch(messages, useUID=uid, rfc822=1)
3270
def fetchHeaders(self, messages, uid=0):
3271
"""Retrieve headers of one or more messages
3273
This command is allowed in the Selected state.
3275
@type messages: C{MessageSet} or C{str}
3276
@param messages: A message sequence set
3279
@param uid: Indicates whether the message sequence set is of message
3280
numbers or of unique message IDs.
3283
@return: A deferred whose callback is invoked with a dict mapping
3284
message numbers to dicts of message headers, or whose errback is
3285
invoked if there is an error.
3287
return self._fetch(messages, useUID=uid, rfc822header=1)
3290
def fetchBody(self, messages, uid=0):
3291
"""Retrieve body text of one or more messages
3293
This command is allowed in the Selected state.
3295
@type messages: C{MessageSet} or C{str}
3296
@param messages: A message sequence set
3299
@param uid: Indicates whether the message sequence set is of message
3300
numbers or of unique message IDs.
3303
@return: A deferred whose callback is invoked with a dict mapping
3304
message numbers to file-like objects containing body text, or whose
3305
errback is invoked if there is an error.
3307
return self._fetch(messages, useUID=uid, rfc822text=1)
3310
def fetchSize(self, messages, uid=0):
3311
"""Retrieve the size, in octets, of one or more messages
3313
This command is allowed in the Selected state.
3315
@type messages: C{MessageSet} or C{str}
3316
@param messages: A message sequence set
3319
@param uid: Indicates whether the message sequence set is of message
3320
numbers or of unique message IDs.
3323
@return: A deferred whose callback is invoked with a dict mapping
3324
message numbers to sizes, or whose errback is invoked if there is
3327
return self._fetch(messages, useUID=uid, rfc822size=1)
3330
def fetchFull(self, messages, uid=0):
3331
"""Retrieve several different fields of one or more messages
3333
This command is allowed in the Selected state. This is equivalent
3334
to issuing all of the C{fetchFlags}, C{fetchInternalDate},
3335
C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody}
3338
@type messages: C{MessageSet} or C{str}
3339
@param messages: A message sequence set
3342
@param uid: Indicates whether the message sequence set is of message
3343
numbers or of unique message IDs.
3346
@return: A deferred whose callback is invoked with a dict mapping
3347
message numbers to dict of the retrieved data values, or whose
3348
errback is invoked if there is an error. They dictionary keys
3349
are "flags", "date", "size", "envelope", and "body".
3352
messages, useUID=uid, flags=1, internaldate=1,
3353
rfc822size=1, envelope=1, body=1)
3356
def fetchAll(self, messages, uid=0):
3357
"""Retrieve several different fields of one or more messages
3359
This command is allowed in the Selected state. This is equivalent
3360
to issuing all of the C{fetchFlags}, C{fetchInternalDate},
3361
C{fetchSize}, and C{fetchEnvelope} functions.
3363
@type messages: C{MessageSet} or C{str}
3364
@param messages: A message sequence set
3367
@param uid: Indicates whether the message sequence set is of message
3368
numbers or of unique message IDs.
3371
@return: A deferred whose callback is invoked with a dict mapping
3372
message numbers to dict of the retrieved data values, or whose
3373
errback is invoked if there is an error. They dictionary keys
3374
are "flags", "date", "size", and "envelope".
3377
messages, useUID=uid, flags=1, internaldate=1,
3378
rfc822size=1, envelope=1)
3381
def fetchFast(self, messages, uid=0):
3382
"""Retrieve several different fields of one or more messages
3384
This command is allowed in the Selected state. This is equivalent
3385
to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and
3386
C{fetchSize} functions.
3388
@type messages: C{MessageSet} or C{str}
3389
@param messages: A message sequence set
3392
@param uid: Indicates whether the message sequence set is of message
3393
numbers or of unique message IDs.
3396
@return: A deferred whose callback is invoked with a dict mapping
3397
message numbers to dict of the retrieved data values, or whose
3398
errback is invoked if there is an error. They dictionary keys are
3399
"flags", "date", and "size".
3402
messages, useUID=uid, flags=1, internaldate=1, rfc822size=1)
3405
def _parseFetchPairs(self, fetchResponseList):
3407
Given the result of parsing a single I{FETCH} response, construct a
3408
C{dict} mapping response keys to response values.
3410
@param fetchResponseList: The result of parsing a I{FETCH} response
3411
with L{parseNestedParens} and extracting just the response data
3412
(that is, just the part that comes after C{"FETCH"}). The form
3413
of this input (and therefore the output of this method) is very
3414
disagreable. A valuable improvement would be to enumerate the
3415
possible keys (representing them as structured objects of some
3416
sort) rather than using strings and tuples of tuples of strings
3417
and so forth. This would allow the keys to be documented more
3418
easily and would allow for a much simpler application-facing API
3419
(one not based on looking up somewhat hard to predict keys in a
3420
dict). Since C{fetchResponseList} notionally represents a
3421
flattened sequence of pairs (identifying keys followed by their
3422
associated values), collapsing such complex elements of this
3423
list as C{["BODY", ["HEADER.FIELDS", ["SUBJECT"]]]} into a
3424
single object would also greatly simplify the implementation of
3427
@return: A C{dict} of the response data represented by C{pairs}. Keys
3428
in this dictionary are things like C{"RFC822.TEXT"}, C{"FLAGS"}, or
3429
C{("BODY", ("HEADER.FIELDS", ("SUBJECT",)))}. Values are entirely
3430
dependent on the key with which they are associated, but retain the
3431
same structured as produced by L{parseNestedParens}.
3434
responseParts = iter(fetchResponseList)
3437
key = responseParts.next()
3438
except StopIteration:
3442
value = responseParts.next()
3443
except StopIteration:
3444
raise IllegalServerResponse(
3445
"Not enough arguments", fetchResponseList)
3447
# The parsed forms of responses like:
3451
# BODY[HEADER.FIELDS (SUBJECT)] VALUE
3452
# BODY[HEADER.FIELDS (SUBJECT)]<N.M> VALUE
3456
# ["BODY", [], VALUE]
3457
# ["BODY", ["TEXT"], VALUE]
3458
# ["BODY", ["HEADER.FIELDS", ["SUBJECT"]], VALUE]
3459
# ["BODY", ["HEADER.FIELDS", ["SUBJECT"]], "<N.M>", VALUE]
3461
# Here, check for these cases and grab as many extra elements as
3462
# necessary to retrieve the body information.
3463
if key in ("BODY", "BODY.PEEK") and isinstance(value, list) and len(value) < 3:
3465
key = (key, tuple(value))
3467
key = (key, (value[0], tuple(value[1])))
3469
value = responseParts.next()
3470
except StopIteration:
3471
raise IllegalServerResponse(
3472
"Not enough arguments", fetchResponseList)
3474
# Handle partial ranges
3475
if value.startswith('<') and value.endswith('>'):
3479
# This isn't really a range, it's some content.
3482
key = key + (value,)
3484
value = responseParts.next()
3485
except StopIteration:
3486
raise IllegalServerResponse(
3487
"Not enough arguments", fetchResponseList)
3493
def _cbFetch(self, (lines, last), requestedParts, structured):
3496
if len(parts) == 3 and parts[1] == 'FETCH':
3497
id = self._intOrRaise(parts[0], parts)
3499
info[id] = [parts[2]]
3501
info[id][0].extend(parts[2])
3504
for (messageId, values) in info.iteritems():
3505
mapping = self._parseFetchPairs(values[0])
3506
results.setdefault(messageId, {}).update(mapping)
3509
for messageId in results.keys():
3510
values = results[messageId]
3511
for part in values.keys():
3512
if part not in requestedParts and part == 'FLAGS':
3513
flagChanges[messageId] = values['FLAGS']
3514
# Find flags in the result and get rid of them.
3515
for i in range(len(info[messageId][0])):
3516
if info[messageId][0][i] == 'FLAGS':
3517
del info[messageId][0][i:i+2]
3521
del results[messageId]
3524
self.flagsChanged(flagChanges)
3532
def fetchSpecific(self, messages, uid=0, headerType=None,
3533
headerNumber=None, headerArgs=None, peek=None,
3534
offset=None, length=None):
3535
"""Retrieve a specific section of one or more messages
3537
@type messages: C{MessageSet} or C{str}
3538
@param messages: A message sequence set
3541
@param uid: Indicates whether the message sequence set is of message
3542
numbers or of unique message IDs.
3544
@type headerType: C{str}
3545
@param headerType: If specified, must be one of HEADER,
3546
HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, or TEXT, and will determine
3547
which part of the message is retrieved. For HEADER.FIELDS and
3548
HEADER.FIELDS.NOT, C{headerArgs} must be a sequence of header names.
3549
For MIME, C{headerNumber} must be specified.
3551
@type headerNumber: C{int} or C{int} sequence
3552
@param headerNumber: The nested rfc822 index specifying the
3553
entity to retrieve. For example, C{1} retrieves the first
3554
entity of the message, and C{(2, 1, 3}) retrieves the 3rd
3555
entity inside the first entity inside the second entity of
3558
@type headerArgs: A sequence of C{str}
3559
@param headerArgs: If C{headerType} is HEADER.FIELDS, these are the
3560
headers to retrieve. If it is HEADER.FIELDS.NOT, these are the
3561
headers to exclude from retrieval.
3564
@param peek: If true, cause the server to not set the \\Seen
3565
flag on this message as a result of this command.
3567
@type offset: C{int}
3568
@param offset: The number of octets at the beginning of the result
3571
@type length: C{int}
3572
@param length: The number of octets to retrieve.
3575
@return: A deferred whose callback is invoked with a mapping of
3576
message numbers to retrieved data, or whose errback is invoked
3577
if there is an error.
3579
fmt = '%s BODY%s[%s%s%s]%s'
3580
if headerNumber is None:
3582
elif isinstance(headerNumber, int):
3583
number = str(headerNumber)
3585
number = '.'.join(map(str, headerNumber))
3586
if headerType is None:
3589
header = '.' + headerType
3592
if header and headerType not in ('TEXT', 'MIME'):
3593
if headerArgs is not None:
3594
payload = ' (%s)' % ' '.join(headerArgs)
3602
extra = '<%d.%d>' % (offset, length)
3603
fetch = uid and 'UID FETCH' or 'FETCH'
3604
cmd = fmt % (messages, peek and '.PEEK' or '', number, header, payload, extra)
3605
d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
3606
d.addCallback(self._cbFetch, (), False)
3610
def _fetch(self, messages, useUID=0, **terms):
3611
fetch = useUID and 'UID FETCH' or 'FETCH'
3613
if 'rfc822text' in terms:
3614
del terms['rfc822text']
3615
terms['rfc822.text'] = True
3616
if 'rfc822size' in terms:
3617
del terms['rfc822size']
3618
terms['rfc822.size'] = True
3619
if 'rfc822header' in terms:
3620
del terms['rfc822header']
3621
terms['rfc822.header'] = True
3623
cmd = '%s (%s)' % (messages, ' '.join([s.upper() for s in terms.keys()]))
3624
d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
3625
d.addCallback(self._cbFetch, map(str.upper, terms.keys()), True)
3628
def setFlags(self, messages, flags, silent=1, uid=0):
3629
"""Set the flags for one or more messages.
3631
This command is allowed in the Selected state.
3633
@type messages: C{MessageSet} or C{str}
3634
@param messages: A message sequence set
3636
@type flags: Any iterable of C{str}
3637
@param flags: The flags to set
3639
@type silent: C{bool}
3640
@param silent: If true, cause the server to supress its verbose
3644
@param uid: Indicates whether the message sequence set is of message
3645
numbers or of unique message IDs.
3648
@return: A deferred whose callback is invoked with a list of the
3649
the server's responses (C{[]} if C{silent} is true) or whose
3650
errback is invoked if there is an error.
3652
return self._store(str(messages), 'FLAGS', silent, flags, uid)
3654
def addFlags(self, messages, flags, silent=1, uid=0):
3655
"""Add to the set flags for one or more messages.
3657
This command is allowed in the Selected state.
3659
@type messages: C{MessageSet} or C{str}
3660
@param messages: A message sequence set
3662
@type flags: Any iterable of C{str}
3663
@param flags: The flags to set
3665
@type silent: C{bool}
3666
@param silent: If true, cause the server to supress its verbose
3670
@param uid: Indicates whether the message sequence set is of message
3671
numbers or of unique message IDs.
3674
@return: A deferred whose callback is invoked with a list of the
3675
the server's responses (C{[]} if C{silent} is true) or whose
3676
errback is invoked if there is an error.
3678
return self._store(str(messages),'+FLAGS', silent, flags, uid)
3680
def removeFlags(self, messages, flags, silent=1, uid=0):
3681
"""Remove from the set flags for one or more messages.
3683
This command is allowed in the Selected state.
3685
@type messages: C{MessageSet} or C{str}
3686
@param messages: A message sequence set
3688
@type flags: Any iterable of C{str}
3689
@param flags: The flags to set
3691
@type silent: C{bool}
3692
@param silent: If true, cause the server to supress its verbose
3696
@param uid: Indicates whether the message sequence set is of message
3697
numbers or of unique message IDs.
3700
@return: A deferred whose callback is invoked with a list of the
3701
the server's responses (C{[]} if C{silent} is true) or whose
3702
errback is invoked if there is an error.
3704
return self._store(str(messages), '-FLAGS', silent, flags, uid)
3707
def _store(self, messages, cmd, silent, flags, uid):
3709
cmd = cmd + '.SILENT'
3710
store = uid and 'UID STORE' or 'STORE'
3711
args = ' '.join((messages, cmd, '(%s)' % ' '.join(flags)))
3712
d = self.sendCommand(Command(store, args, wantResponse=('FETCH',)))
3715
expected = ('FLAGS',)
3716
d.addCallback(self._cbFetch, expected, True)
3720
def copy(self, messages, mailbox, uid):
3721
"""Copy the specified messages to the specified mailbox.
3723
This command is allowed in the Selected state.
3725
@type messages: C{str}
3726
@param messages: A message sequence set
3728
@type mailbox: C{str}
3729
@param mailbox: The mailbox to which to copy the messages
3732
@param uid: If true, the C{messages} refers to message UIDs, rather
3733
than message sequence numbers.
3736
@return: A deferred whose callback is invoked with a true value
3737
when the copy is successful, or whose errback is invoked if there
3744
args = '%s %s' % (messages, _prepareMailboxName(mailbox))
3745
return self.sendCommand(Command(cmd, args))
3748
# IMailboxListener methods
3750
def modeChanged(self, writeable):
3753
def flagsChanged(self, newFlags):
3756
def newMessages(self, exists, recent):
3760
class IllegalIdentifierError(IMAP4Exception): pass
3764
parts = s.split(',')
3767
low, high = p.split(':', 1)
3777
res.extend((low, high))
3779
raise IllegalIdentifierError(p)
3787
raise IllegalIdentifierError(p)
3792
class IllegalQueryError(IMAP4Exception): pass
3795
'ALL', 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'NEW', 'OLD', 'RECENT',
3796
'SEEN', 'UNANSWERED', 'UNDELETED', 'UNDRAFT', 'UNFLAGGED', 'UNSEEN'
3800
'LARGER', 'SMALLER', 'UID'
3803
def Query(sorted=0, **kwarg):
3804
"""Create a query string
3806
Among the accepted keywords are::
3808
all : If set to a true value, search all messages in the
3811
answered : If set to a true value, search messages flagged with
3814
bcc : A substring to search the BCC header field for
3816
before : Search messages with an internal date before this
3817
value. The given date should be a string in the format
3818
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3820
body : A substring to search the body of the messages for
3822
cc : A substring to search the CC header field for
3824
deleted : If set to a true value, search messages flagged with
3827
draft : If set to a true value, search messages flagged with
3830
flagged : If set to a true value, search messages flagged with
3833
from : A substring to search the From header field for
3835
header : A two-tuple of a header name and substring to search
3838
keyword : Search for messages with the given keyword set
3840
larger : Search for messages larger than this number of octets
3842
messages : Search only the given message sequence set.
3844
new : If set to a true value, search messages flagged with
3845
\\Recent but not \\Seen
3847
old : If set to a true value, search messages not flagged with
3850
on : Search messages with an internal date which is on this
3851
date. The given date should be a string in the format
3852
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3854
recent : If set to a true value, search for messages flagged with
3857
seen : If set to a true value, search for messages flagged with
3860
sentbefore : Search for messages with an RFC822 'Date' header before
3861
this date. The given date should be a string in the format
3862
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3864
senton : Search for messages with an RFC822 'Date' header which is
3865
on this date The given date should be a string in the format
3866
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3868
sentsince : Search for messages with an RFC822 'Date' header which is
3869
after this date. The given date should be a string in the format
3870
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3872
since : Search for messages with an internal date that is after
3873
this date.. The given date should be a string in the format
3874
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3876
smaller : Search for messages smaller than this number of octets
3878
subject : A substring to search the 'subject' header for
3880
text : A substring to search the entire message for
3882
to : A substring to search the 'to' header for
3884
uid : Search only the messages in the given message set
3886
unanswered : If set to a true value, search for messages not
3887
flagged with \\Answered
3889
undeleted : If set to a true value, search for messages not
3890
flagged with \\Deleted
3892
undraft : If set to a true value, search for messages not
3893
flagged with \\Draft
3895
unflagged : If set to a true value, search for messages not
3896
flagged with \\Flagged
3898
unkeyword : Search for messages without the given keyword set
3900
unseen : If set to a true value, search for messages not
3903
@type sorted: C{bool}
3904
@param sorted: If true, the output will be sorted, alphabetically.
3905
The standard does not require it, but it makes testing this function
3906
easier. The default is zero, and this should be acceptable for any
3910
@return: The formatted query string
3919
if k in _SIMPLE_BOOL and v:
3922
cmd.extend([k, v[0], '"%s"' % (v[1],)])
3923
elif k not in _NO_QUOTES:
3924
cmd.extend([k, '"%s"' % (v,)])
3926
cmd.extend([k, '%s' % (v,)])
3928
return '(%s)' % ' '.join(cmd)
3930
return ' '.join(cmd)
3933
"""The disjunction of two or more queries"""
3935
raise IllegalQueryError, args
3936
elif len(args) == 2:
3937
return '(OR %s %s)' % args
3939
return '(OR %s %s)' % (args[0], Or(*args[1:]))
3942
"""The negation of a query"""
3943
return '(NOT %s)' % (query,)
3945
class MismatchedNesting(IMAP4Exception):
3948
class MismatchedQuoting(IMAP4Exception):
3951
def wildcardToRegexp(wildcard, delim=None):
3952
wildcard = wildcard.replace('*', '(?:.*?)')
3954
wildcard = wildcard.replace('%', '(?:.*?)')
3956
wildcard = wildcard.replace('%', '(?:(?:[^%s])*?)' % re.escape(delim))
3957
return re.compile(wildcard, re.I)
3960
"""Split a string into whitespace delimited tokens
3962
Tokens that would otherwise be separated but are surrounded by \"
3963
remain as a single token. Any token that is not quoted and is
3964
equal to \"NIL\" is tokenized as C{None}.
3967
@param s: The string to be split
3969
@rtype: C{list} of C{str}
3970
@return: A list of the resulting tokens
3972
@raise MismatchedQuoting: Raised if an odd number of quotes are present
3977
inQuote = inWord = False
3978
for i, c in enumerate(s):
3980
if i and s[i-1] == '\\':
3987
result.append(''.join(word))
3989
elif not inWord and not inQuote and c not in ('"' + string.whitespace):
3992
elif inWord and not inQuote and c in string.whitespace:
4000
elif inWord or inQuote:
4004
raise MismatchedQuoting(s)
4016
def splitOn(sequence, predicate, transformers):
4018
mode = predicate(sequence[0])
4020
for e in sequence[1:]:
4023
result.extend(transformers[mode](tmp))
4028
result.extend(transformers[mode](tmp))
4031
def collapseStrings(results):
4033
Turns a list of length-one strings and lists into a list of longer
4034
strings and lists. For example,
4036
['a', 'b', ['c', 'd']] is returned as ['ab', ['cd']]
4038
@type results: C{list} of C{str} and C{list}
4039
@param results: The list to be collapsed
4041
@rtype: C{list} of C{str} and C{list}
4042
@return: A new list which is the collapsed form of C{results}
4046
listsList = [isinstance(s, types.ListType) for s in results]
4048
pred = lambda e: isinstance(e, types.TupleType)
4050
0: lambda e: splitQuoted(''.join(e)),
4051
1: lambda e: [''.join([i[0] for i in e])]
4053
for (i, c, isList) in zip(range(len(results)), results, listsList):
4055
if begun is not None:
4056
copy.extend(splitOn(results[begun:i], pred, tran))
4058
copy.append(collapseStrings(c))
4061
if begun is not None:
4062
copy.extend(splitOn(results[begun:], pred, tran))
4066
def parseNestedParens(s, handleLiteral = 1):
4067
"""Parse an s-exp-like string into a more useful data structure.
4070
@param s: The s-exp-like string to parse
4072
@rtype: C{list} of C{str} and C{list}
4073
@return: A list containing the tokens present in the input.
4075
@raise MismatchedNesting: Raised if the number or placement
4076
of opening or closing parenthesis is invalid.
4088
contentStack[-1].append(s[i:i+2])
4092
inQuote = not inQuote
4093
contentStack[-1].append(c)
4097
contentStack[-1].append(c)
4098
inQuote = not inQuote
4100
elif handleLiteral and c == '{':
4101
end = s.find('}', i)
4103
raise ValueError, "Malformed literal"
4104
literalSize = int(s[i+1:end])
4105
contentStack[-1].append((s[end+3:end+3+literalSize],))
4106
i = end + 3 + literalSize
4107
elif c == '(' or c == '[':
4108
contentStack.append([])
4110
elif c == ')' or c == ']':
4111
contentStack[-2].append(contentStack.pop())
4114
contentStack[-1].append(c)
4117
raise MismatchedNesting(s)
4118
if len(contentStack) != 1:
4119
raise MismatchedNesting(s)
4120
return collapseStrings(contentStack[0])
4123
return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),)
4126
return '{%d}\r\n%s' % (len(s), s)
4129
def __init__(self, value):
4133
return str(self.value)
4135
_ATOM_SPECIALS = '(){ %*"'
4140
if c < '\x20' or c > '\x7f':
4142
if c in _ATOM_SPECIALS:
4146
def _prepareMailboxName(name):
4147
name = name.encode('imap4-utf-7')
4148
if _needsQuote(name):
4152
def _needsLiteral(s):
4153
# Change this to "return 1" to wig out stupid clients
4154
return '\n' in s or '\r' in s or len(s) > 1000
4156
def collapseNestedLists(items):
4157
"""Turn a nested list structure into an s-exp-like string.
4159
Strings in C{items} will be sent as literals if they contain CR or LF,
4160
otherwise they will be quoted. References to None in C{items} will be
4161
translated to the atom NIL. Objects with a 'read' attribute will have
4162
it called on them with no arguments and the returned string will be
4163
inserted into the output as a literal. Integers will be converted to
4164
strings and inserted into the output unquoted. Instances of
4165
C{DontQuoteMe} will be converted to strings and inserted into the output
4168
This function used to be much nicer, and only quote things that really
4169
needed to be quoted (and C{DontQuoteMe} did not exist), however, many
4170
broken IMAP4 clients were unable to deal with this level of sophistication,
4171
forcing the current behavior to be adopted for practical reasons.
4173
@type items: Any iterable
4180
pieces.extend([' ', 'NIL'])
4181
elif isinstance(i, (DontQuoteMe, int, long)):
4182
pieces.extend([' ', str(i)])
4183
elif isinstance(i, types.StringTypes):
4184
if _needsLiteral(i):
4185
pieces.extend([' ', '{', str(len(i)), '}', IMAP4Server.delimiter, i])
4187
pieces.extend([' ', _quote(i)])
4188
elif hasattr(i, 'read'):
4190
pieces.extend([' ', '{', str(len(d)), '}', IMAP4Server.delimiter, d])
4192
pieces.extend([' ', '(%s)' % (collapseNestedLists(i),)])
4193
return ''.join(pieces[1:])
4196
class IClientAuthentication(Interface):
4198
"""Return an identifier associated with this authentication scheme.
4203
def challengeResponse(secret, challenge):
4204
"""Generate a challenge response string"""
4208
class CramMD5ClientAuthenticator:
4209
implements(IClientAuthentication)
4211
def __init__(self, user):
4217
def challengeResponse(self, secret, chal):
4218
response = hmac.HMAC(secret, chal).hexdigest()
4219
return '%s %s' % (self.user, response)
4223
class LOGINAuthenticator:
4224
implements(IClientAuthentication)
4226
def __init__(self, user):
4228
self.challengeResponse = self.challengeUsername
4233
def challengeUsername(self, secret, chal):
4234
# Respond to something like "Username:"
4235
self.challengeResponse = self.challengeSecret
4238
def challengeSecret(self, secret, chal):
4239
# Respond to something like "Password:"
4242
class PLAINAuthenticator:
4243
implements(IClientAuthentication)
4245
def __init__(self, user):
4251
def challengeResponse(self, secret, chal):
4252
return '\0%s\0%s' % (self.user, secret)
4255
class MailboxException(IMAP4Exception): pass
4257
class MailboxCollision(MailboxException):
4259
return 'Mailbox named %s already exists' % self.args
4261
class NoSuchMailbox(MailboxException):
4263
return 'No mailbox named %s exists' % self.args
4265
class ReadOnlyMailbox(MailboxException):
4267
return 'Mailbox open in read-only state'
4270
class IAccount(Interface):
4271
"""Interface for Account classes
4273
Implementors of this interface should consider implementing
4274
C{INamespacePresenter}.
4277
def addMailbox(name, mbox = None):
4278
"""Add a new mailbox to this account
4281
@param name: The name associated with this mailbox. It may not
4282
contain multiple hierarchical parts.
4284
@type mbox: An object implementing C{IMailbox}
4285
@param mbox: The mailbox to associate with this name. If C{None},
4286
a suitable default is created and used.
4288
@rtype: C{Deferred} or C{bool}
4289
@return: A true value if the creation succeeds, or a deferred whose
4290
callback will be invoked when the creation succeeds.
4292
@raise MailboxException: Raised if this mailbox cannot be added for
4293
some reason. This may also be raised asynchronously, if a C{Deferred}
4297
def create(pathspec):
4298
"""Create a new mailbox from the given hierarchical name.
4300
@type pathspec: C{str}
4301
@param pathspec: The full hierarchical name of a new mailbox to create.
4302
If any of the inferior hierarchical names to this one do not exist,
4303
they are created as well.
4305
@rtype: C{Deferred} or C{bool}
4306
@return: A true value if the creation succeeds, or a deferred whose
4307
callback will be invoked when the creation succeeds.
4309
@raise MailboxException: Raised if this mailbox cannot be added.
4310
This may also be raised asynchronously, if a C{Deferred} is
4314
def select(name, rw=True):
4315
"""Acquire a mailbox, given its name.
4318
@param name: The mailbox to acquire
4321
@param rw: If a true value, request a read-write version of this
4322
mailbox. If a false value, request a read-only version.
4324
@rtype: Any object implementing C{IMailbox} or C{Deferred}
4325
@return: The mailbox object, or a C{Deferred} whose callback will
4326
be invoked with the mailbox object. None may be returned if the
4327
specified mailbox may not be selected for any reason.
4331
"""Delete the mailbox with the specified name.
4334
@param name: The mailbox to delete.
4336
@rtype: C{Deferred} or C{bool}
4337
@return: A true value if the mailbox is successfully deleted, or a
4338
C{Deferred} whose callback will be invoked when the deletion
4341
@raise MailboxException: Raised if this mailbox cannot be deleted.
4342
This may also be raised asynchronously, if a C{Deferred} is returned.
4345
def rename(oldname, newname):
4348
@type oldname: C{str}
4349
@param oldname: The current name of the mailbox to rename.
4351
@type newname: C{str}
4352
@param newname: The new name to associate with the mailbox.
4354
@rtype: C{Deferred} or C{bool}
4355
@return: A true value if the mailbox is successfully renamed, or a
4356
C{Deferred} whose callback will be invoked when the rename operation
4359
@raise MailboxException: Raised if this mailbox cannot be
4360
renamed. This may also be raised asynchronously, if a C{Deferred}
4364
def isSubscribed(name):
4365
"""Check the subscription status of a mailbox
4368
@param name: The name of the mailbox to check
4370
@rtype: C{Deferred} or C{bool}
4371
@return: A true value if the given mailbox is currently subscribed
4372
to, a false value otherwise. A C{Deferred} may also be returned
4373
whose callback will be invoked with one of these values.
4376
def subscribe(name):
4377
"""Subscribe to a mailbox
4380
@param name: The name of the mailbox to subscribe to
4382
@rtype: C{Deferred} or C{bool}
4383
@return: A true value if the mailbox is subscribed to successfully,
4384
or a Deferred whose callback will be invoked with this value when
4385
the subscription is successful.
4387
@raise MailboxException: Raised if this mailbox cannot be
4388
subscribed to. This may also be raised asynchronously, if a
4389
C{Deferred} is returned.
4392
def unsubscribe(name):
4393
"""Unsubscribe from a mailbox
4396
@param name: The name of the mailbox to unsubscribe from
4398
@rtype: C{Deferred} or C{bool}
4399
@return: A true value if the mailbox is unsubscribed from successfully,
4400
or a Deferred whose callback will be invoked with this value when
4401
the unsubscription is successful.
4403
@raise MailboxException: Raised if this mailbox cannot be
4404
unsubscribed from. This may also be raised asynchronously, if a
4405
C{Deferred} is returned.
4408
def listMailboxes(ref, wildcard):
4409
"""List all the mailboxes that meet a certain criteria
4412
@param ref: The context in which to apply the wildcard
4414
@type wildcard: C{str}
4415
@param wildcard: An expression against which to match mailbox names.
4416
'*' matches any number of characters in a mailbox name, and '%'
4417
matches similarly, but will not match across hierarchical boundaries.
4419
@rtype: C{list} of C{tuple}
4420
@return: A list of C{(mailboxName, mailboxObject)} which meet the
4421
given criteria. C{mailboxObject} should implement either
4422
C{IMailboxInfo} or C{IMailbox}. A Deferred may also be returned.
4425
class INamespacePresenter(Interface):
4426
def getPersonalNamespaces():
4427
"""Report the available personal namespaces.
4429
Typically there should be only one personal namespace. A common
4430
name for it is \"\", and its hierarchical delimiter is usually
4433
@rtype: iterable of two-tuples of strings
4434
@return: The personal namespaces and their hierarchical delimiters.
4435
If no namespaces of this type exist, None should be returned.
4438
def getSharedNamespaces():
4439
"""Report the available shared namespaces.
4441
Shared namespaces do not belong to any individual user but are
4442
usually to one or more of them. Examples of shared namespaces
4443
might be \"#news\" for a usenet gateway.
4445
@rtype: iterable of two-tuples of strings
4446
@return: The shared namespaces and their hierarchical delimiters.
4447
If no namespaces of this type exist, None should be returned.
4450
def getUserNamespaces():
4451
"""Report the available user namespaces.
4453
These are namespaces that contain folders belonging to other users
4454
access to which this account has been granted.
4456
@rtype: iterable of two-tuples of strings
4457
@return: The user namespaces and their hierarchical delimiters.
4458
If no namespaces of this type exist, None should be returned.
4462
class MemoryAccount(object):
4463
implements(IAccount, INamespacePresenter)
4466
subscriptions = None
4469
def __init__(self, name):
4472
self.subscriptions = []
4474
def allocateID(self):
4482
def addMailbox(self, name, mbox = None):
4484
if self.mailboxes.has_key(name):
4485
raise MailboxCollision, name
4487
mbox = self._emptyMailbox(name, self.allocateID())
4488
self.mailboxes[name] = mbox
4491
def create(self, pathspec):
4492
paths = filter(None, pathspec.split('/'))
4493
for accum in range(1, len(paths)):
4495
self.addMailbox('/'.join(paths[:accum]))
4496
except MailboxCollision:
4499
self.addMailbox('/'.join(paths))
4500
except MailboxCollision:
4501
if not pathspec.endswith('/'):
4505
def _emptyMailbox(self, name, id):
4506
raise NotImplementedError
4508
def select(self, name, readwrite=1):
4509
return self.mailboxes.get(name.upper())
4511
def delete(self, name):
4513
# See if this mailbox exists at all
4514
mbox = self.mailboxes.get(name)
4516
raise MailboxException("No such mailbox")
4517
# See if this box is flagged \Noselect
4518
if r'\Noselect' in mbox.getFlags():
4519
# Check for hierarchically inferior mailboxes with this one
4520
# as part of their root.
4521
for others in self.mailboxes.keys():
4522
if others != name and others.startswith(name):
4523
raise MailboxException, "Hierarchically inferior mailboxes exist and \\Noselect is set"
4526
# iff there are no hierarchically inferior names, we will
4527
# delete it from our ken.
4528
if self._inferiorNames(name) > 1:
4529
del self.mailboxes[name]
4531
def rename(self, oldname, newname):
4532
oldname = oldname.upper()
4533
newname = newname.upper()
4534
if not self.mailboxes.has_key(oldname):
4535
raise NoSuchMailbox, oldname
4537
inferiors = self._inferiorNames(oldname)
4538
inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
4540
for (old, new) in inferiors:
4541
if self.mailboxes.has_key(new):
4542
raise MailboxCollision, new
4544
for (old, new) in inferiors:
4545
self.mailboxes[new] = self.mailboxes[old]
4546
del self.mailboxes[old]
4548
def _inferiorNames(self, name):
4550
for infname in self.mailboxes.keys():
4551
if infname.startswith(name):
4552
inferiors.append(infname)
4555
def isSubscribed(self, name):
4556
return name.upper() in self.subscriptions
4558
def subscribe(self, name):
4560
if name not in self.subscriptions:
4561
self.subscriptions.append(name)
4563
def unsubscribe(self, name):
4565
if name not in self.subscriptions:
4566
raise MailboxException, "Not currently subscribed to " + name
4567
self.subscriptions.remove(name)
4569
def listMailboxes(self, ref, wildcard):
4570
ref = self._inferiorNames(ref.upper())
4571
wildcard = wildcardToRegexp(wildcard, '/')
4572
return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]
4575
## INamespacePresenter
4577
def getPersonalNamespaces(self):
4580
def getSharedNamespaces(self):
4583
def getOtherNamespaces(self):
4588
_statusRequestDict = {
4589
'MESSAGES': 'getMessageCount',
4590
'RECENT': 'getRecentCount',
4591
'UIDNEXT': 'getUIDNext',
4592
'UIDVALIDITY': 'getUIDValidity',
4593
'UNSEEN': 'getUnseenCount'
4595
def statusRequestHelper(mbox, names):
4598
r[n] = getattr(mbox, _statusRequestDict[n.upper()])()
4601
def parseAddr(addr):
4603
return [(None, None, None),]
4604
addrs = email.Utils.getaddresses([addr])
4605
return [[fn or None, None] + addr.split('@') for fn, addr in addrs]
4607
def getEnvelope(msg):
4608
headers = msg.getHeaders(True)
4609
date = headers.get('date')
4610
subject = headers.get('subject')
4611
from_ = headers.get('from')
4612
sender = headers.get('sender', from_)
4613
reply_to = headers.get('reply-to', from_)
4614
to = headers.get('to')
4615
cc = headers.get('cc')
4616
bcc = headers.get('bcc')
4617
in_reply_to = headers.get('in-reply-to')
4618
mid = headers.get('message-id')
4619
return (date, subject, parseAddr(from_), parseAddr(sender),
4620
reply_to and parseAddr(reply_to), to and parseAddr(to),
4621
cc and parseAddr(cc), bcc and parseAddr(bcc), in_reply_to, mid)
4623
def getLineCount(msg):
4624
# XXX - Super expensive, CACHE THIS VALUE FOR LATER RE-USE
4625
# XXX - This must be the number of lines in the ENCODED version
4627
for _ in msg.getBodyFile():
4632
if s[0] == s[-1] == '"':
4636
def getBodyStructure(msg, extended=False):
4637
# XXX - This does not properly handle multipart messages
4638
# BODYSTRUCTURE is obscenely complex and criminally under-documented.
4641
headers = 'content-type', 'content-id', 'content-description', 'content-transfer-encoding'
4642
headers = msg.getHeaders(False, *headers)
4643
mm = headers.get('content-type')
4645
mm = ''.join(mm.splitlines())
4646
mimetype = mm.split(';')
4648
type = mimetype[0].split('/', 1)
4652
elif len(type) == 2:
4655
major = minor = None
4656
attrs = dict([x.strip().lower().split('=', 1) for x in mimetype[1:]])
4658
major = minor = None
4660
major = minor = None
4663
size = str(msg.getSize())
4664
unquotedAttrs = [(k, unquote(v)) for (k, v) in attrs.iteritems()]
4666
major, minor, # Main and Sub MIME types
4667
unquotedAttrs, # content-type parameter list
4668
headers.get('content-id'),
4669
headers.get('content-description'),
4670
headers.get('content-transfer-encoding'),
4671
size, # Number of octets total
4674
if major is not None:
4675
if major.lower() == 'text':
4676
result.append(str(getLineCount(msg)))
4677
elif (major.lower(), minor.lower()) == ('message', 'rfc822'):
4678
contained = msg.getSubPart(0)
4679
result.append(getEnvelope(contained))
4680
result.append(getBodyStructure(contained, False))
4681
result.append(str(getLineCount(contained)))
4683
if not extended or major is None:
4686
if major.lower() != 'multipart':
4687
headers = 'content-md5', 'content-disposition', 'content-language'
4688
headers = msg.getHeaders(False, *headers)
4689
disp = headers.get('content-disposition')
4691
# XXX - I dunno if this is really right
4693
disp = disp.split('; ')
4695
disp = (disp[0].lower(), None)
4697
disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
4699
result.append(headers.get('content-md5'))
4701
result.append(headers.get('content-language'))
4707
submsg = msg.getSubPart(i)
4708
result.append(getBodyStructure(submsg))
4711
result.append(minor)
4712
result.append(attrs.items())
4714
# XXX - I dunno if this is really right
4715
headers = msg.getHeaders(False, 'content-disposition', 'content-language')
4716
disp = headers.get('content-disposition')
4718
disp = disp.split('; ')
4720
disp = (disp[0].lower(), None)
4722
disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
4725
result.append(headers.get('content-language'))
4729
class IMessagePart(Interface):
4730
def getHeaders(negate, *names):
4731
"""Retrieve a group of message headers.
4733
@type names: C{tuple} of C{str}
4734
@param names: The names of the headers to retrieve or omit.
4736
@type negate: C{bool}
4737
@param negate: If True, indicates that the headers listed in C{names}
4738
should be omitted from the return value, rather than included.
4741
@return: A mapping of header field names to header field values
4745
"""Retrieve a file object containing only the body of this message.
4749
"""Retrieve the total size, in octets, of this message.
4755
"""Indicate whether this message has subparts.
4760
def getSubPart(part):
4761
"""Retrieve a MIME sub-message
4764
@param part: The number of the part to retrieve, indexed from 0.
4766
@raise IndexError: Raised if the specified part does not exist.
4767
@raise TypeError: Raised if this message is not multipart.
4769
@rtype: Any object implementing C{IMessagePart}.
4770
@return: The specified sub-part.
4773
class IMessage(IMessagePart):
4775
"""Retrieve the unique identifier associated with this message.
4779
"""Retrieve the flags associated with this message.
4782
@return: The flags, represented as strings.
4785
def getInternalDate():
4786
"""Retrieve the date internally associated with this message.
4789
@return: An RFC822-formatted date string.
4792
class IMessageFile(Interface):
4793
"""Optional message interface for representing messages as files.
4795
If provided by message objects, this interface will be used instead
4796
the more complex MIME-based interface.
4799
"""Return an file-like object opened for reading.
4801
Reading from the returned file will return all the bytes
4802
of which this message consists.
4805
class ISearchableMailbox(Interface):
4806
def search(query, uid):
4807
"""Search for messages that meet the given query criteria.
4809
If this interface is not implemented by the mailbox, L{IMailbox.fetch}
4810
and various methods of L{IMessage} will be used instead.
4812
Implementations which wish to offer better performance than the
4813
default implementation should implement this interface.
4815
@type query: C{list}
4816
@param query: The search criteria
4819
@param uid: If true, the IDs specified in the query are UIDs;
4820
otherwise they are message sequence IDs.
4822
@rtype: C{list} or C{Deferred}
4823
@return: A list of message sequence numbers or message UIDs which
4824
match the search criteria or a C{Deferred} whose callback will be
4825
invoked with such a list.
4828
class IMessageCopier(Interface):
4829
def copy(messageObject):
4830
"""Copy the given message object into this mailbox.
4832
The message object will be one which was previously returned by
4835
Implementations which wish to offer better performance than the
4836
default implementation should implement this interface.
4838
If this interface is not implemented by the mailbox, IMailbox.addMessage
4839
will be used instead.
4841
@rtype: C{Deferred} or C{int}
4842
@return: Either the UID of the message or a Deferred which fires
4843
with the UID when the copy finishes.
4846
class IMailboxInfo(Interface):
4847
"""Interface specifying only the methods required for C{listMailboxes}.
4849
Implementations can return objects implementing only these methods for
4850
return to C{listMailboxes} if it can allow them to operate more
4855
"""Return the flags defined in this mailbox
4857
Flags with the \\ prefix are reserved for use as system flags.
4859
@rtype: C{list} of C{str}
4860
@return: A list of the flags that can be set on messages in this mailbox.
4863
def getHierarchicalDelimiter():
4864
"""Get the character which delimits namespaces for in this mailbox.
4869
class IMailbox(IMailboxInfo):
4870
def getUIDValidity():
4871
"""Return the unique validity identifier for this mailbox.
4877
"""Return the likely UID for the next message added to this mailbox.
4882
def getUID(message):
4883
"""Return the UID of a message in the mailbox
4885
@type message: C{int}
4886
@param message: The message sequence number
4889
@return: The UID of the message.
4892
def getMessageCount():
4893
"""Return the number of messages in this mailbox.
4898
def getRecentCount():
4899
"""Return the number of messages with the 'Recent' flag.
4904
def getUnseenCount():
4905
"""Return the number of messages with the 'Unseen' flag.
4911
"""Get the read/write status of the mailbox.
4914
@return: A true value if write permission is allowed, a false value otherwise.
4918
"""Called before this mailbox is deleted, permanently.
4920
If necessary, all resources held by this mailbox should be cleaned
4921
up here. This function _must_ set the \\Noselect flag on this
4925
def requestStatus(names):
4926
"""Return status information about this mailbox.
4928
Mailboxes which do not intend to do any special processing to
4929
generate the return value, C{statusRequestHelper} can be used
4930
to build the dictionary by calling the other interface methods
4931
which return the data for each name.
4933
@type names: Any iterable
4934
@param names: The status names to return information regarding.
4935
The possible values for each name are: MESSAGES, RECENT, UIDNEXT,
4936
UIDVALIDITY, UNSEEN.
4938
@rtype: C{dict} or C{Deferred}
4939
@return: A dictionary containing status information about the
4940
requested names is returned. If the process of looking this
4941
information up would be costly, a deferred whose callback will
4942
eventually be passed this dictionary is returned instead.
4945
def addListener(listener):
4946
"""Add a mailbox change listener
4948
@type listener: Any object which implements C{IMailboxListener}
4949
@param listener: An object to add to the set of those which will
4950
be notified when the contents of this mailbox change.
4953
def removeListener(listener):
4954
"""Remove a mailbox change listener
4956
@type listener: Any object previously added to and not removed from
4957
this mailbox as a listener.
4958
@param listener: The object to remove from the set of listeners.
4960
@raise ValueError: Raised when the given object is not a listener for
4964
def addMessage(message, flags = (), date = None):
4965
"""Add the given message to this mailbox.
4967
@type message: A file-like object
4968
@param message: The RFC822 formatted message
4970
@type flags: Any iterable of C{str}
4971
@param flags: The flags to associate with this message
4974
@param date: If specified, the date to associate with this
4978
@return: A deferred whose callback is invoked with the message
4979
id if the message is added successfully and whose errback is
4982
@raise ReadOnlyMailbox: Raised if this Mailbox is not open for
4987
"""Remove all messages flagged \\Deleted.
4989
@rtype: C{list} or C{Deferred}
4990
@return: The list of message sequence numbers which were deleted,
4991
or a C{Deferred} whose callback will be invoked with such a list.
4993
@raise ReadOnlyMailbox: Raised if this Mailbox is not open for
4997
def fetch(messages, uid):
4998
"""Retrieve one or more messages.
5000
@type messages: C{MessageSet}
5001
@param messages: The identifiers of messages to retrieve information
5005
@param uid: If true, the IDs specified in the query are UIDs;
5006
otherwise they are message sequence IDs.
5008
@rtype: Any iterable of two-tuples of message sequence numbers and
5009
implementors of C{IMessage}.
5012
def store(messages, flags, mode, uid):
5013
"""Set the flags of one or more messages.
5015
@type messages: A MessageSet object with the list of messages requested
5016
@param messages: The identifiers of the messages to set the flags of.
5018
@type flags: sequence of C{str}
5019
@param flags: The flags to set, unset, or add.
5021
@type mode: -1, 0, or 1
5022
@param mode: If mode is -1, these flags should be removed from the
5023
specified messages. If mode is 1, these flags should be added to
5024
the specified messages. If mode is 0, all existing flags should be
5025
cleared and these flags should be added.
5028
@param uid: If true, the IDs specified in the query are UIDs;
5029
otherwise they are message sequence IDs.
5031
@rtype: C{dict} or C{Deferred}
5032
@return: A C{dict} mapping message sequence numbers to sequences of C{str}
5033
representing the flags set on the message after this operation has
5034
been performed, or a C{Deferred} whose callback will be invoked with
5037
@raise ReadOnlyMailbox: Raised if this mailbox is not open for
5041
class ICloseableMailbox(Interface):
5042
"""A supplementary interface for mailboxes which require cleanup on close.
5044
Implementing this interface is optional. If it is implemented, the protocol
5045
code will call the close method defined whenever a mailbox is closed.
5048
"""Close this mailbox.
5050
@return: A C{Deferred} which fires when this mailbox
5051
has been closed, or None if the mailbox can be closed
5055
def _formatHeaders(headers):
5056
hdrs = [': '.join((k.title(), '\r\n'.join(v.splitlines()))) for (k, v)
5057
in headers.iteritems()]
5058
hdrs = '\r\n'.join(hdrs) + '\r\n'
5065
yield m.getSubPart(i)
5070
def iterateInReactor(i):
5071
"""Consume an interator at most a single iteration per reactor iteration.
5073
If the iterator produces a Deferred, the next iteration will not occur
5074
until the Deferred fires, otherwise the next iteration will be taken
5075
in the next reactor iteration.
5078
@return: A deferred which fires (with None) when the iterator is
5079
exhausted or whose errback is called if there is an exception.
5081
from twisted.internet import reactor
5082
d = defer.Deferred()
5086
except StopIteration:
5091
if isinstance(r, defer.Deferred):
5094
reactor.callLater(0, go, r)
5098
class MessageProducer:
5099
CHUNK_SIZE = 2 ** 2 ** 2 ** 2
5101
def __init__(self, msg, buffer = None, scheduler = None):
5102
"""Produce this message.
5104
@param msg: The message I am to produce.
5105
@type msg: L{IMessage}
5107
@param buffer: A buffer to hold the message in. If None, I will
5108
use a L{tempfile.TemporaryFile}.
5109
@type buffer: file-like
5113
buffer = tempfile.TemporaryFile()
5114
self.buffer = buffer
5115
if scheduler is None:
5116
scheduler = iterateInReactor
5117
self.scheduler = scheduler
5118
self.write = self.buffer.write
5120
def beginProducing(self, consumer):
5121
self.consumer = consumer
5122
return self.scheduler(self._produce())
5125
headers = self.msg.getHeaders(True)
5127
if self.msg.isMultipart():
5128
content = headers.get('content-type')
5129
parts = [x.split('=', 1) for x in content.split(';')[1:]]
5130
parts = dict([(k.lower().strip(), v) for (k, v) in parts])
5131
boundary = parts.get('boundary')
5132
if boundary is None:
5134
boundary = '----=_%f_boundary_%f' % (time.time(), random.random())
5135
headers['content-type'] += '; boundary="%s"' % (boundary,)
5137
if boundary.startswith('"') and boundary.endswith('"'):
5138
boundary = boundary[1:-1]
5140
self.write(_formatHeaders(headers))
5142
if self.msg.isMultipart():
5143
for p in subparts(self.msg):
5144
self.write('\r\n--%s\r\n' % (boundary,))
5145
yield MessageProducer(p, self.buffer, self.scheduler
5146
).beginProducing(None
5148
self.write('\r\n--%s--\r\n' % (boundary,))
5150
f = self.msg.getBodyFile()
5152
b = f.read(self.CHUNK_SIZE)
5154
self.buffer.write(b)
5159
self.buffer.seek(0, 0)
5160
yield FileProducer(self.buffer
5161
).beginProducing(self.consumer
5162
).addCallback(lambda _: self
5167
# Response should be a list of fields from the message:
5168
# date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
5171
# from, sender, reply-to, to, cc, and bcc are themselves lists of
5172
# address information:
5173
# personal name, source route, mailbox name, host name
5175
# reply-to and sender must not be None. If not present in a message
5176
# they should be defaulted to the value of the from field.
5178
__str__ = lambda self: 'envelope'
5182
__str__ = lambda self: 'flags'
5185
type = 'internaldate'
5186
__str__ = lambda self: 'internaldate'
5189
type = 'rfc822header'
5190
__str__ = lambda self: 'rfc822.header'
5194
__str__ = lambda self: 'rfc822.text'
5198
__str__ = lambda self: 'rfc822.size'
5202
__str__ = lambda self: 'rfc822'
5206
__str__ = lambda self: 'uid'
5217
partialLength = None
5223
part = '.'.join([str(x + 1) for x in self.part])
5228
base += '[%s%s%s]' % (part, separator, self.header,)
5230
base += '[%s%sTEXT]' % (part, separator)
5232
base += '[%s%sMIME]' % (part, separator)
5234
base += '[%s]' % (part,)
5235
if self.partialBegin is not None:
5236
base += '<%d.%d>' % (self.partialBegin, self.partialLength)
5239
class BodyStructure:
5240
type = 'bodystructure'
5241
__str__ = lambda self: 'bodystructure'
5243
# These three aren't top-level, they don't need type indicators
5255
for f in self.fields:
5260
base += ' (%s)' % ' '.join(fields)
5262
base = '.'.join([str(x + 1) for x in self.part]) + '.' + base
5273
_simple_fetch_att = [
5274
('envelope', Envelope),
5276
('internaldate', InternalDate),
5277
('rfc822.header', RFC822Header),
5278
('rfc822.text', RFC822Text),
5279
('rfc822.size', RFC822Size),
5282
('bodystructure', BodyStructure),
5286
self.state = ['initial']
5290
def parseString(self, s):
5291
s = self.remaining + s
5293
while s or self.state:
5294
# print 'Entering state_' + self.state[-1] + ' with', repr(s)
5295
state = self.state.pop()
5297
used = getattr(self, 'state_' + state)(s)
5299
self.state.append(state)
5302
# print state, 'consumed', repr(s[:used])
5307
def state_initial(self, s):
5308
# In the initial state, the literals "ALL", "FULL", and "FAST"
5309
# are accepted, as is a ( indicating the beginning of a fetch_att
5310
# token, as is the beginning of a fetch_att token.
5315
if l.startswith('all'):
5316
self.result.extend((
5317
self.Flags(), self.InternalDate(),
5318
self.RFC822Size(), self.Envelope()
5321
if l.startswith('full'):
5322
self.result.extend((
5323
self.Flags(), self.InternalDate(),
5324
self.RFC822Size(), self.Envelope(),
5328
if l.startswith('fast'):
5329
self.result.extend((
5330
self.Flags(), self.InternalDate(), self.RFC822Size(),
5334
if l.startswith('('):
5335
self.state.extend(('close_paren', 'maybe_fetch_att', 'fetch_att'))
5338
self.state.append('fetch_att')
5341
def state_close_paren(self, s):
5342
if s.startswith(')'):
5344
raise Exception("Missing )")
5346
def state_whitespace(self, s):
5347
# Eat up all the leading whitespace
5348
if not s or not s[0].isspace():
5349
raise Exception("Whitespace expected, none found")
5351
for i in range(len(s)):
5352
if not s[i].isspace():
5356
def state_maybe_fetch_att(self, s):
5357
if not s.startswith(')'):
5358
self.state.extend(('maybe_fetch_att', 'fetch_att', 'whitespace'))
5361
def state_fetch_att(self, s):
5362
# Allowed fetch_att tokens are "ENVELOPE", "FLAGS", "INTERNALDATE",
5363
# "RFC822", "RFC822.HEADER", "RFC822.SIZE", "RFC822.TEXT", "BODY",
5364
# "BODYSTRUCTURE", "UID",
5365
# "BODY [".PEEK"] [<section>] ["<" <number> "." <nz_number> ">"]
5368
for (name, cls) in self._simple_fetch_att:
5369
if l.startswith(name):
5370
self.result.append(cls())
5374
if l.startswith('body.peek'):
5377
elif l.startswith('body'):
5380
raise Exception("Nothing recognized in fetch_att: %s" % (l,))
5382
self.pending_body = b
5383
self.state.extend(('got_body', 'maybe_partial', 'maybe_section'))
5386
def state_got_body(self, s):
5387
self.result.append(self.pending_body)
5388
del self.pending_body
5391
def state_maybe_section(self, s):
5392
if not s.startswith("["):
5395
self.state.extend(('section', 'part_number'))
5398
_partExpr = re.compile(r'(\d+(?:\.\d+)*)\.?')
5399
def state_part_number(self, s):
5400
m = self._partExpr.match(s)
5402
self.parts = [int(p) - 1 for p in m.groups()[0].split('.')]
5408
def state_section(self, s):
5409
# Grab "HEADER]" or "HEADER.FIELDS (Header list)]" or
5410
# "HEADER.FIELDS.NOT (Header list)]" or "TEXT]" or "MIME]" or
5415
if l.startswith(']'):
5416
self.pending_body.empty = True
5418
elif l.startswith('header]'):
5419
h = self.pending_body.header = self.Header()
5423
elif l.startswith('text]'):
5424
self.pending_body.text = self.Text()
5426
elif l.startswith('mime]'):
5427
self.pending_body.mime = self.MIME()
5431
if l.startswith('header.fields.not'):
5434
elif l.startswith('header.fields'):
5437
raise Exception("Unhandled section contents: %r" % (l,))
5439
self.pending_body.header = h
5440
self.state.extend(('finish_section', 'header_list', 'whitespace'))
5441
self.pending_body.part = tuple(self.parts)
5445
def state_finish_section(self, s):
5446
if not s.startswith(']'):
5447
raise Exception("section must end with ]")
5450
def state_header_list(self, s):
5451
if not s.startswith('('):
5452
raise Exception("Header list must begin with (")
5455
raise Exception("Header list must end with )")
5457
headers = s[1:end].split()
5458
self.pending_body.header.fields = map(str.upper, headers)
5461
def state_maybe_partial(self, s):
5462
# Grab <number.number> or nothing at all
5463
if not s.startswith('<'):
5467
raise Exception("Found < but not >")
5470
parts = partial.split('.', 1)
5472
raise Exception("Partial specification did not include two .-delimited integers")
5473
begin, length = map(int, parts)
5474
self.pending_body.partialBegin = begin
5475
self.pending_body.partialLength = length
5480
CHUNK_SIZE = 2 ** 2 ** 2 ** 2
5484
def __init__(self, f):
5487
def beginProducing(self, consumer):
5488
self.consumer = consumer
5489
self.produce = consumer.write
5490
d = self._onDone = defer.Deferred()
5491
self.consumer.registerProducer(self, False)
5494
def resumeProducing(self):
5497
b = '{%d}\r\n' % self._size()
5498
self.firstWrite = False
5501
b = b + self.f.read(self.CHUNK_SIZE)
5503
self.consumer.unregisterProducer()
5504
self._onDone.callback(self)
5505
self._onDone = self.f = self.consumer = None
5509
def pauseProducing(self):
5512
def stopProducing(self):
5523
# XXX - This may require localization :(
5525
'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct',
5526
'nov', 'dec', 'january', 'february', 'march', 'april', 'may', 'june',
5527
'july', 'august', 'september', 'october', 'november', 'december'
5530
'day': r"(?P<day>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
5531
'mon': r"(?P<mon>\w+)",
5532
'year': r"(?P<year>\d\d\d\d)"
5534
m = re.match('%(day)s-%(mon)s-%(year)s' % expr, s)
5536
raise ValueError, "Cannot parse time string %r" % (s,)
5539
d['mon'] = 1 + (months.index(d['mon'].lower()) % 12)
5540
d['year'] = int(d['year'])
5541
d['day'] = int(d['day'])
5543
raise ValueError, "Cannot parse time string %r" % (s,)
5545
return time.struct_time(
5546
(d['year'], d['mon'], d['day'], 0, 0, 0, -1, -1, -1)
5550
def modified_base64(s):
5551
s_utf7 = s.encode('utf-7')
5552
return s_utf7[1:-1].replace('/', ',')
5554
def modified_unbase64(s):
5555
s_utf7 = '+' + s.replace(',', '/') + '-'
5556
return s_utf7.decode('utf-7')
5558
def encoder(s, errors=None):
5560
Encode the given C{unicode} string using the IMAP4 specific variation of
5564
@param s: The text to encode.
5566
@param errors: Policy for handling encoding errors. Currently ignored.
5568
@return: C{tuple} of a C{str} giving the encoded bytes and an C{int}
5569
giving the number of code units consumed from the input.
5574
if ord(c) in (range(0x20, 0x26) + range(0x27, 0x7f)):
5576
r.extend(['&', modified_base64(''.join(_in)), '-'])
5581
r.extend(['&', modified_base64(''.join(_in)), '-'])
5587
r.extend(['&', modified_base64(''.join(_in)), '-'])
5588
return (''.join(r), len(s))
5590
def decoder(s, errors=None):
5592
Decode the given C{str} using the IMAP4 specific variation of UTF-7.
5595
@param s: The bytes to decode.
5597
@param errors: Policy for handling decoding errors. Currently ignored.
5599
@return: a C{tuple} of a C{unicode} string giving the text which was
5600
decoded and an C{int} giving the number of bytes consumed from the
5606
if c == '&' and not decode:
5608
elif c == '-' and decode:
5609
if len(decode) == 1:
5612
r.append(modified_unbase64(''.join(decode[1:])))
5619
r.append(modified_unbase64(''.join(decode[1:])))
5620
return (''.join(r), len(s))
5622
class StreamReader(codecs.StreamReader):
5623
def decode(self, s, errors='strict'):
5626
class StreamWriter(codecs.StreamWriter):
5627
def encode(self, s, errors='strict'):
5630
_codecInfo = (encoder, decoder, StreamReader, StreamWriter)
5632
_codecInfoClass = codecs.CodecInfo
5633
except AttributeError:
5636
_codecInfo = _codecInfoClass(*_codecInfo)
5638
def imap4_utf_7(name):
5639
if name == 'imap4-utf-7':
5641
codecs.register(imap4_utf_7)
5645
'IMAP4Server', 'IMAP4Client',
5648
'IMailboxListener', 'IClientAuthentication', 'IAccount', 'IMailbox',
5649
'INamespacePresenter', 'ICloseableMailbox', 'IMailboxInfo',
5650
'IMessage', 'IMessageCopier', 'IMessageFile', 'ISearchableMailbox',
5653
'IMAP4Exception', 'IllegalClientResponse', 'IllegalOperation',
5654
'IllegalMailboxEncoding', 'UnhandledResponse', 'NegativeResponse',
5655
'NoSupportedAuthentication', 'IllegalServerResponse',
5656
'IllegalIdentifierError', 'IllegalQueryError', 'MismatchedNesting',
5657
'MismatchedQuoting', 'MailboxException', 'MailboxCollision',
5658
'NoSuchMailbox', 'ReadOnlyMailbox',
5661
'CramMD5ClientAuthenticator', 'PLAINAuthenticator', 'LOGINAuthenticator',
5662
'PLAINCredentials', 'LOGINCredentials',
5664
# Simple query interface
5665
'Query', 'Not', 'Or',
5669
'statusRequestHelper',