1
# -*- test-case-name: twisted.mail.test.test_imap -*-
2
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
3
# See LICENSE for details.
7
An IMAP4 protocol implementation
9
API Stability: Semi-stable
11
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
14
Suspend idle timeout while server is processing
15
Use an async message parser instead of buffering in memory
16
Figure out a way to not queue multi-message client requests (Flow? A simple callback?)
17
Clarify some API docs (Query, etc)
18
Make APPEND recognize (again) non-existent mailboxes before accepting the literal
21
from __future__ import nested_scopes
22
from __future__ import generators
24
from twisted.protocols import basic
25
from twisted.protocols import policies
26
from twisted.internet import defer
27
from twisted.internet import error
28
from twisted.internet.defer import maybeDeferred
29
from twisted.python import log, util, failure, text
30
from twisted.internet import interfaces
32
from twisted import cred
33
import twisted.cred.error
34
import twisted.cred.credentials
48
from zope.interface import implements, Interface
53
import cStringIO as StringIO
57
class MessageSet(object):
59
Essentially an infinite bitfield, with some extra features.
61
@type getnext: Function taking C{int} returning C{int}
62
@ivar getnext: A function that returns the next message number,
63
used when iterating through the MessageSet. By default, a function
64
returning the next integer is supplied, but as this can be rather
65
inefficient for sparse UID iterations, it is recommended to supply
66
one when messages are requested by UID. The argument is provided
67
as a hint to the implementation and may be ignored if it makes sense
68
to do so (eg, if an iterator is being used that maintains its own
69
state, it is guaranteed that it will not be called out-of-order).
73
def __init__(self, start=_empty, end=_empty):
75
Create a new MessageSet()
77
@type start: Optional C{int}
78
@param start: Start of range, or only message number
80
@type end: Optional C{int}
81
@param end: End of range.
83
self._last = self._empty # Last message/UID in use
84
self.ranges = [] # List of ranges included
85
self.getnext = lambda x: x+1 # A function which will return the next
86
# message id. Handy for UID requests.
88
if start is self._empty:
91
if isinstance(start, types.ListType):
92
self.ranges = start[:]
99
def _setLast(self,value):
100
if self._last is not self._empty:
101
raise ValueError("last already set")
104
for i,(l,h) in enumerate(self.ranges):
106
break # There are no more Nones after this
112
self.ranges[i] = (l,h)
120
"Highest" message number, refered to by "*".
121
Must be set before attempting to use the MessageSet.
123
return _getLast, _setLast, None, doc
124
last = property(*last())
126
def add(self, start, end=_empty):
131
@param start: Start of range, or only message number
133
@type end: Optional C{int}
134
@param end: End of range.
136
if end is self._empty:
139
if self._last is not self._empty:
146
# Try to keep in low, high order if possible
147
# (But we don't know what None means, this will keep
148
# None at the start of the ranges list)
149
start, end = end, start
151
self.ranges.append((start,end))
154
def __add__(self, other):
155
if isinstance(other, MessageSet):
156
ranges = self.ranges + other.ranges
157
return MessageSet(ranges)
159
res = MessageSet(self.ranges)
166
def extend(self, other):
167
if isinstance(other, MessageSet):
168
self.ranges.extend(other.ranges)
180
Clean ranges list, combining adjacent ranges
185
oldl, oldh = None, None
186
for i,(l,h) in enumerate(self.ranges):
189
# l is >= oldl and h is >= oldh due to sort()
190
if oldl is not None and l <= oldh+1:
193
self.ranges[i-1] = None
194
self.ranges[i] = (l,h)
198
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")
214
for l,h in self.ranges:
215
l = self.getnext(l-1)
223
if self.ranges and self.ranges[0][0] is None:
224
raise TypeError("Can't iterate; last value not set")
226
return self._iterator()
230
for l, h in self.ranges:
232
raise TypeError("Can't size object; last value not set")
239
for low, high in self.ranges:
246
p.append('%d:*' % (high,))
248
p.append('%d:%d' % (low, high))
252
return '<MessageSet %s>' % (str(self),)
254
def __eq__(self, other):
255
if isinstance(other, MessageSet):
256
return self.ranges == other.ranges
261
def __init__(self, size, defered):
266
def write(self, data):
267
self.size -= len(data)
270
self.data.append(data)
273
data, passon = data[:self.size], data[self.size:]
277
self.data.append(data)
280
def callback(self, line):
282
Call defered with data and rest of line
284
self.defer.callback((''.join(self.data), line))
287
_memoryFileLimit = 1024 * 1024 * 10
289
def __init__(self, size, defered):
292
if size > self._memoryFileLimit:
293
self.data = tempfile.TemporaryFile()
295
self.data = StringIO.StringIO()
297
def write(self, data):
298
self.size -= len(data)
301
self.data.write(data)
304
data, passon = data[:self.size], data[self.size:]
308
self.data.write(data)
311
def callback(self, line):
313
Call defered with data and rest of line
316
self.defer.callback((self.data, line))
320
"""Buffer up a bunch of writes before sending them all to a transport at once.
322
def __init__(self, transport, size=8192):
323
self.bufferSize = size
324
self.transport = transport
329
self._length += len(s)
330
self._writes.append(s)
331
if self._length > self.bufferSize:
336
self.transport.writeSequence(self._writes)
342
_1_RESPONSES = ('CAPABILITY', 'FLAGS', 'LIST', 'LSUB', 'STATUS', 'SEARCH', 'NAMESPACE')
343
_2_RESPONSES = ('EXISTS', 'EXPUNGE', 'FETCH', 'RECENT')
344
_OK_RESPONSES = ('UIDVALIDITY', 'READ-WRITE', 'READ-ONLY', 'UIDNEXT', 'PERMANENTFLAGS')
347
def __init__(self, command, args=None, wantResponse=(),
348
continuation=None, *contArgs, **contKw):
349
self.command = command
351
self.wantResponse = wantResponse
352
self.continuation = lambda x: continuation(x, *contArgs, **contKw)
355
def format(self, tag):
356
if self.args is None:
357
return ' '.join((tag, self.command))
358
return ' '.join((tag, self.command, self.args))
360
def finish(self, lastLine, unusedCallback):
364
names = parseNestedParens(L)
366
if (N >= 1 and names[0] in self._1_RESPONSES or
367
N >= 2 and names[0] == 'OK' and isinstance(names[1], types.ListType) and names[1][0] in self._OK_RESPONSES):
369
elif N >= 3 and names[1] in self._2_RESPONSES:
370
if isinstance(names[2], list) and len(names[2]) >= 1 and names[2][0] == 'FLAGS' and 'FLAGS' not in self.args:
374
elif N >= 2 and names[1] in self._2_RESPONSES:
378
d, self.defer = self.defer, None
379
d.callback((send, lastLine))
381
unusedCallback(unuse)
383
class LOGINCredentials(cred.credentials.UsernamePassword):
385
self.challenges = ['Password\0', 'User Name\0']
386
self.responses = ['password', 'username']
387
cred.credentials.UsernamePassword.__init__(self, None, None)
389
def getChallenge(self):
390
return self.challenges.pop()
392
def setResponse(self, response):
393
setattr(self, self.responses.pop(), response)
395
def moreChallenges(self):
396
return bool(self.challenges)
398
class PLAINCredentials(cred.credentials.UsernamePassword):
400
cred.credentials.UsernamePassword.__init__(self, None, None)
402
def getChallenge(self):
405
def setResponse(self, response):
406
parts = response[:-1].split('\0', 1)
408
raise IllegalClientResponse("Malformed Response - wrong number of parts")
409
self.username, self.password = parts
411
def moreChallenges(self):
414
class IMAP4Exception(Exception):
415
def __init__(self, *args):
416
Exception.__init__(self, *args)
418
class IllegalClientResponse(IMAP4Exception): pass
420
class IllegalOperation(IMAP4Exception): pass
422
class IllegalMailboxEncoding(IMAP4Exception): pass
424
class IMailboxListener(Interface):
425
"""Interface for objects interested in mailbox events"""
427
def modeChanged(writeable):
428
"""Indicates that the write status of a mailbox has changed.
430
@type writeable: C{bool}
431
@param writeable: A true value if write is now allowed, false
435
def flagsChanged(newFlags):
436
"""Indicates that the flags of one or more messages have changed.
438
@type newFlags: C{dict}
439
@param newFlags: A mapping of message identifiers to tuples of flags
440
now set on that message.
443
def newMessages(exists, recent):
444
"""Indicates that the number of messages in a mailbox has changed.
446
@type exists: C{int} or C{None}
447
@param exists: The total number of messages now in this mailbox.
448
If the total number of messages has not changed, this should be
452
@param recent: The number of messages now flagged \\Recent.
453
If the number of recent messages has not changed, this should be
457
class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin):
459
Protocol implementation for an IMAP4rev1 server.
461
The server can be in any of four states:
467
implements(IMailboxListener)
469
# Identifier for this server software
470
IDENT = 'Twisted IMAP4rev1 Ready'
472
# Number of seconds before idle timeout
473
# Initially 1 minute. Raised to 30 minutes after login.
476
POSTAUTH_TIMEOUT = 60 * 30
478
# Whether STARTTLS has been issued successfully yet or not.
481
# Whether our transport supports TLS
484
# Mapping of tags to commands we have received
487
# The object which will handle logins for us
490
# The account object for this connection
496
# The currently selected mailbox
499
# Command data to be processed when literal data is received
500
_pendingLiteral = None
502
# Maximum length to accept for a "short" string literal
503
_literalStringLimit = 4096
505
# IChallengeResponse factories for AUTHENTICATE command
510
parseState = 'command'
512
def __init__(self, chal = None, contextFactory = None, scheduler = None):
515
self.challengers = chal
516
self.ctx = contextFactory
517
if scheduler is None:
518
scheduler = iterateInReactor
519
self._scheduler = scheduler
520
self._queuedAsync = []
522
def capabilities(self):
523
cap = {'AUTH': self.challengers.keys()}
524
if self.ctx and self.canStartTLS:
525
if not self.startedTLS and interfaces.ISSLTransport(self.transport, None) is None:
526
cap['LOGINDISABLED'] = None
527
cap['STARTTLS'] = None
528
cap['NAMESPACE'] = None
532
def connectionMade(self):
534
self.canStartTLS = interfaces.ITLSTransport(self.transport, None) is not None
535
self.setTimeout(self.timeOut)
536
self.sendServerGreeting()
538
def connectionLost(self, reason):
539
self.setTimeout(None)
542
self._onLogout = None
544
def timeoutConnection(self):
545
self.sendLine('* BYE Autologout; connection idle too long')
546
self.transport.loseConnection()
548
self.mbox.removeListener(self)
549
cmbx = ICloseableMailbox(self.mbox, None)
551
maybeDeferred(cmbx.close).addErrback(log.err)
553
self.state = 'timeout'
555
def rawDataReceived(self, data):
557
passon = self._pendingLiteral.write(data)
558
if passon is not None:
559
self.setLineMode(passon)
561
# Avoid processing commands while buffers are being dumped to
566
commands = self.blocked
568
while commands and self.blocked is None:
569
self.lineReceived(commands.pop(0))
570
if self.blocked is not None:
571
self.blocked.extend(commands)
573
# def sendLine(self, line):
574
# print 'C:', repr(line)
575
# return basic.LineReceiver.sendLine(self, line)
577
def lineReceived(self, line):
578
# print 'S:', repr(line)
579
if self.blocked is not None:
580
self.blocked.append(line)
585
f = getattr(self, 'parse_' + self.parseState)
589
self.sendUntaggedResponse('BAD Server error: ' + str(e))
592
def parse_command(self, line):
593
args = line.split(None, 2)
596
tag, cmd, rest = args
601
self.sendBadResponse(tag, 'Missing command')
604
self.sendBadResponse(None, 'Null command')
609
return self.dispatchCommand(tag, cmd, rest)
610
except IllegalClientResponse, e:
611
self.sendBadResponse(tag, 'Illegal syntax: ' + str(e))
612
except IllegalOperation, e:
613
self.sendNegativeResponse(tag, 'Illegal operation: ' + str(e))
614
except IllegalMailboxEncoding, e:
615
self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + str(e))
617
def parse_pending(self, line):
618
d = self._pendingLiteral
619
self._pendingLiteral = None
620
self.parseState = 'command'
623
def dispatchCommand(self, tag, cmd, rest, uid=None):
624
f = self.lookupCommand(cmd)
628
self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid)
630
self.sendBadResponse(tag, 'Unsupported command')
632
def lookupCommand(self, cmd):
633
return getattr(self, '_'.join((self.state, cmd.upper())), None)
635
def __doCommand(self, tag, handler, args, parseargs, line, uid):
636
for (i, arg) in enumerate(parseargs):
638
parseargs = parseargs[i+1:]
639
maybeDeferred(arg, self, line).addCallback(
640
self.__cbDispatch, tag, handler, args,
641
parseargs, uid).addErrback(self.__ebDispatch, tag)
648
raise IllegalClientResponse("Too many arguments for command: " + repr(line))
651
handler(uid=uid, *args)
655
def __cbDispatch(self, (arg, rest), tag, fn, args, parseargs, uid):
657
self.__doCommand(tag, fn, args, parseargs, rest, uid)
659
def __ebDispatch(self, failure, tag):
660
if failure.check(IllegalClientResponse):
661
self.sendBadResponse(tag, 'Illegal syntax: ' + str(failure.value))
662
elif failure.check(IllegalOperation):
663
self.sendNegativeResponse(tag, 'Illegal operation: ' +
665
elif failure.check(IllegalMailboxEncoding):
666
self.sendNegativeResponse(tag, 'Illegal mailbox name: ' +
669
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
672
def _stringLiteral(self, size):
673
if size > self._literalStringLimit:
674
raise IllegalClientResponse(
675
"Literal too long! I accept at most %d octets" %
676
(self._literalStringLimit,))
678
self.parseState = 'pending'
679
self._pendingLiteral = LiteralString(size, d)
680
self.sendContinuationRequest('Ready for %d octets of text' % size)
684
def _fileLiteral(self, size):
686
self.parseState = 'pending'
687
self._pendingLiteral = LiteralFile(size, d)
688
self.sendContinuationRequest('Ready for %d octets of data' % size)
692
def arg_astring(self, line):
694
Parse an astring from the line, return (arg, rest), possibly
695
via a deferred (to handle literals)
699
raise IllegalClientResponse("Missing argument")
701
arg, rest = None, None
704
spam, arg, rest = line.split('"',2)
705
rest = rest[1:] # Strip space
707
raise IllegalClientResponse("Unmatched quotes")
711
raise IllegalClientResponse("Malformed literal")
713
size = int(line[1:-1])
715
raise IllegalClientResponse("Bad literal size: " + line[1:-1])
716
d = self._stringLiteral(size)
718
arg = line.split(' ',1)
722
return d or (arg, rest)
724
# ATOM: Any CHAR except ( ) { % * " \ ] CTL SP (CHAR is 7bit)
725
atomre = re.compile(r'(?P<atom>[^\](){%*"\\\x00-\x20\x80-\xff]+)( (?P<rest>.*$)|$)')
727
def arg_atom(self, line):
729
Parse an atom from the line
732
raise IllegalClientResponse("Missing argument")
733
m = self.atomre.match(line)
735
return m.group('atom'), m.group('rest')
737
raise IllegalClientResponse("Malformed ATOM")
739
def arg_plist(self, line):
741
Parse a (non-nested) parenthesised list from the line
744
raise IllegalClientResponse("Missing argument")
747
raise IllegalClientResponse("Missing parenthesis")
752
raise IllegalClientResponse("Mismatched parenthesis")
754
return (parseNestedParens(line[1:i],0), line[i+2:])
756
def arg_literal(self, line):
758
Parse a literal from the line
761
raise IllegalClientResponse("Missing argument")
764
raise IllegalClientResponse("Missing literal")
767
raise IllegalClientResponse("Malformed literal")
770
size = int(line[1:-1])
772
raise IllegalClientResponse("Bad literal size: " + line[1:-1])
774
return self._fileLiteral(size)
776
def arg_searchkeys(self, line):
780
query = parseNestedParens(line)
781
# XXX Should really use list of search terms and parse into
786
def arg_seqset(self, line):
791
arg = line.split(' ',1)
797
return (parseIdList(arg), rest)
798
except IllegalIdentifierError, e:
799
raise IllegalClientResponse("Bad message number " + str(e))
801
def arg_fetchatt(self, line):
807
return (p.result, '')
809
def arg_flaglist(self, line):
811
Flag part of store-att-flag
816
raise IllegalClientResponse("Mismatched parenthesis")
820
m = self.atomre.search(line)
822
raise IllegalClientResponse("Malformed flag")
823
if line[0] == '\\' and m.start() == 1:
824
flags.append('\\' + m.group('atom'))
826
flags.append(m.group('atom'))
828
raise IllegalClientResponse("Malformed flag")
829
line = m.group('rest')
833
def arg_line(self, line):
835
Command line of UID command
839
def opt_plist(self, line):
841
Optional parenthesised list
843
if line.startswith('('):
844
return self.arg_plist(line)
848
def opt_datetime(self, line):
850
Optional date-time string
852
if line.startswith('"'):
854
spam, date, rest = line.split('"',2)
856
raise IllegalClientResponse("Malformed date-time")
857
return (date, rest[1:])
861
def opt_charset(self, line):
863
Optional charset of SEARCH command
865
if line[:7].upper() == 'CHARSET':
866
arg = line.split(' ',2)
868
raise IllegalClientResponse("Missing charset identifier")
871
spam, arg, rest = arg
876
def sendServerGreeting(self):
877
msg = '[CAPABILITY %s] %s' % (' '.join(self.listCapabilities()), self.IDENT)
878
self.sendPositiveResponse(message=msg)
880
def sendBadResponse(self, tag = None, message = ''):
881
self._respond('BAD', tag, message)
883
def sendPositiveResponse(self, tag = None, message = ''):
884
self._respond('OK', tag, message)
886
def sendNegativeResponse(self, tag = None, message = ''):
887
self._respond('NO', tag, message)
889
def sendUntaggedResponse(self, message, async=False):
890
if not async or (self.blocked is None):
891
self._respond(message, None, None)
893
self._queuedAsync.append(message)
895
def sendContinuationRequest(self, msg = 'Ready for additional command text'):
897
self.sendLine('+ ' + msg)
901
def _respond(self, state, tag, message):
902
if state in ('OK', 'NO', 'BAD') and self._queuedAsync:
903
lines = self._queuedAsync
904
self._queuedAsync = []
906
self._respond(msg, None, None)
910
self.sendLine(' '.join((tag, state, message)))
912
self.sendLine(' '.join((tag, state)))
914
def listCapabilities(self):
916
for c, v in self.capabilities().iteritems():
920
caps.extend([('%s=%s' % (c, cap)) for cap in v])
923
def do_CAPABILITY(self, tag):
924
self.sendUntaggedResponse('CAPABILITY ' + ' '.join(self.listCapabilities()))
925
self.sendPositiveResponse(tag, 'CAPABILITY completed')
927
unauth_CAPABILITY = (do_CAPABILITY,)
928
auth_CAPABILITY = unauth_CAPABILITY
929
select_CAPABILITY = unauth_CAPABILITY
930
logout_CAPABILITY = unauth_CAPABILITY
932
def do_LOGOUT(self, tag):
933
self.sendUntaggedResponse('BYE Nice talking to you')
934
self.sendPositiveResponse(tag, 'LOGOUT successful')
935
self.transport.loseConnection()
937
unauth_LOGOUT = (do_LOGOUT,)
938
auth_LOGOUT = unauth_LOGOUT
939
select_LOGOUT = unauth_LOGOUT
940
logout_LOGOUT = unauth_LOGOUT
942
def do_NOOP(self, tag):
943
self.sendPositiveResponse(tag, 'NOOP No operation performed')
945
unauth_NOOP = (do_NOOP,)
946
auth_NOOP = unauth_NOOP
947
select_NOOP = unauth_NOOP
948
logout_NOOP = unauth_NOOP
950
def do_AUTHENTICATE(self, tag, args):
951
args = args.upper().strip()
952
if args not in self.challengers:
953
self.sendNegativeResponse(tag, 'AUTHENTICATE method unsupported')
955
self.authenticate(self.challengers[args](), tag)
957
unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom)
959
def authenticate(self, chal, tag):
960
if self.portal is None:
961
self.sendNegativeResponse(tag, 'Temporary authentication failure')
964
self._setupChallenge(chal, tag)
966
def _setupChallenge(self, chal, tag):
968
challenge = chal.getChallenge()
970
self.sendBadResponse(tag, 'Server error: ' + str(e))
972
coded = base64.encodestring(challenge)[:-1]
973
self.parseState = 'pending'
974
self._pendingLiteral = defer.Deferred()
975
self.sendContinuationRequest(coded)
976
self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag)
977
self._pendingLiteral.addErrback(self.__ebAuthChunk, tag)
979
def __cbAuthChunk(self, result, chal, tag):
981
uncoded = base64.decodestring(result)
982
except binascii.Error:
983
raise IllegalClientResponse("Malformed Response - not base64")
985
chal.setResponse(uncoded)
986
if chal.moreChallenges():
987
self._setupChallenge(chal, tag)
989
self.portal.login(chal, None, IAccount).addCallbacks(
992
(tag,), None, (tag,), None
995
def __cbAuthResp(self, (iface, avatar, logout), tag):
996
assert iface is IAccount, "IAccount is the only supported interface"
997
self.account = avatar
999
self._onLogout = logout
1000
self.sendPositiveResponse(tag, 'Authentication successful')
1001
self.setTimeout(self.POSTAUTH_TIMEOUT)
1003
def __ebAuthResp(self, failure, tag):
1004
if failure.check(cred.error.UnauthorizedLogin):
1005
self.sendNegativeResponse(tag, 'Authentication failed: unauthorized')
1006
elif failure.check(cred.error.UnhandledCredentials):
1007
self.sendNegativeResponse(tag, 'Authentication failed: server misconfigured')
1009
self.sendBadResponse(tag, 'Server error: login failed unexpectedly')
1012
def __ebAuthChunk(self, failure, tag):
1013
self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.value))
1015
def do_STARTTLS(self, tag):
1017
self.sendNegativeResponse(tag, 'TLS already negotiated')
1018
elif self.ctx and self.canStartTLS:
1019
self.sendPositiveResponse(tag, 'Begin TLS negotiation now')
1020
self.transport.startTLS(self.ctx)
1021
self.startedTLS = True
1022
self.challengers = self.challengers.copy()
1023
if 'LOGIN' not in self.challengers:
1024
self.challengers['LOGIN'] = LOGINCredentials
1025
if 'PLAIN' not in self.challengers:
1026
self.challengers['PLAIN'] = PLAINCredentials
1028
self.sendNegativeResponse(tag, 'TLS not available')
1030
unauth_STARTTLS = (do_STARTTLS,)
1032
def do_LOGIN(self, tag, user, passwd):
1033
if 'LOGINDISABLED' in self.capabilities():
1034
self.sendBadResponse(tag, 'LOGIN is disabled before STARTTLS')
1037
maybeDeferred(self.authenticateLogin, user, passwd
1038
).addCallback(self.__cbLogin, tag
1039
).addErrback(self.__ebLogin, tag
1042
unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring)
1044
def authenticateLogin(self, user, passwd):
1045
"""Lookup the account associated with the given parameters
1047
Override this method to define the desired authentication behavior.
1049
The default behavior is to defer authentication to C{self.portal}
1050
if it is not None, or to deny the login otherwise.
1053
@param user: The username to lookup
1055
@type passwd: C{str}
1056
@param passwd: The password to login with
1059
return self.portal.login(
1060
cred.credentials.UsernamePassword(user, passwd),
1063
raise cred.error.UnauthorizedLogin()
1065
def __cbLogin(self, (iface, avatar, logout), tag):
1066
if iface is not IAccount:
1067
self.sendBadResponse(tag, 'Server error: login returned unexpected value')
1068
log.err("__cbLogin called with %r, IAccount expected" % (iface,))
1070
self.account = avatar
1071
self._onLogout = logout
1072
self.sendPositiveResponse(tag, 'LOGIN succeeded')
1074
self.setTimeout(self.POSTAUTH_TIMEOUT)
1076
def __ebLogin(self, failure, tag):
1077
if failure.check(cred.error.UnauthorizedLogin):
1078
self.sendNegativeResponse(tag, 'LOGIN failed')
1080
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1083
def do_NAMESPACE(self, tag):
1084
personal = public = shared = None
1085
np = INamespacePresenter(self.account, None)
1087
personal = np.getPersonalNamespaces()
1088
public = np.getSharedNamespaces()
1089
shared = np.getSharedNamespaces()
1090
self.sendUntaggedResponse('NAMESPACE ' + collapseNestedLists([personal, public, shared]))
1091
self.sendPositiveResponse(tag, "NAMESPACE command completed")
1093
auth_NAMESPACE = (do_NAMESPACE,)
1094
select_NAMESPACE = auth_NAMESPACE
1096
def _parseMbox(self, name):
1097
if isinstance(name, unicode):
1100
return name.decode('imap4-utf-7')
1103
raise IllegalMailboxEncoding(name)
1105
def _selectWork(self, tag, name, rw, cmdName):
1107
self.mbox.removeListener(self)
1108
cmbx = ICloseableMailbox(self.mbox, None)
1109
if cmbx is not None:
1110
maybeDeferred(cmbx.close).addErrback(log.err)
1114
name = self._parseMbox(name)
1115
maybeDeferred(self.account.select, self._parseMbox(name), rw
1116
).addCallback(self._cbSelectWork, cmdName, tag
1117
).addErrback(self._ebSelectWork, cmdName, tag
1120
def _ebSelectWork(self, failure, cmdName, tag):
1121
self.sendBadResponse(tag, "%s failed: Server error" % (cmdName,))
1124
def _cbSelectWork(self, mbox, cmdName, tag):
1126
self.sendNegativeResponse(tag, 'No such mailbox')
1128
if '\\noselect' in [s.lower() for s in mbox.getFlags()]:
1129
self.sendNegativeResponse(tag, 'Mailbox cannot be selected')
1132
flags = mbox.getFlags()
1133
self.sendUntaggedResponse(str(mbox.getMessageCount()) + ' EXISTS')
1134
self.sendUntaggedResponse(str(mbox.getRecentCount()) + ' RECENT')
1135
self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags))
1136
self.sendPositiveResponse(None, '[UIDVALIDITY %d]' % mbox.getUIDValidity())
1138
s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY'
1139
mbox.addListener(self)
1140
self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName))
1141
self.state = 'select'
1144
auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' )
1145
select_SELECT = auth_SELECT
1147
auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' )
1148
select_EXAMINE = auth_EXAMINE
1151
def do_IDLE(self, tag):
1152
self.sendContinuationRequest(None)
1154
self.lastState = self.parseState
1155
self.parseState = 'idle'
1157
def parse_idle(self, *args):
1158
self.parseState = self.lastState
1160
self.sendPositiveResponse(self.parseTag, "IDLE terminated")
1163
select_IDLE = ( do_IDLE, )
1164
auth_IDLE = select_IDLE
1167
def do_CREATE(self, tag, name):
1168
name = self._parseMbox(name)
1170
result = self.account.create(name)
1171
except MailboxException, c:
1172
self.sendNegativeResponse(tag, str(c))
1174
self.sendBadResponse(tag, "Server error encountered while creating mailbox")
1178
self.sendPositiveResponse(tag, 'Mailbox created')
1180
self.sendNegativeResponse(tag, 'Mailbox not created')
1182
auth_CREATE = (do_CREATE, arg_astring)
1183
select_CREATE = auth_CREATE
1185
def do_DELETE(self, tag, name):
1186
name = self._parseMbox(name)
1187
if name.lower() == 'inbox':
1188
self.sendNegativeResponse(tag, 'You cannot delete the inbox')
1191
self.account.delete(name)
1192
except MailboxException, m:
1193
self.sendNegativeResponse(tag, str(m))
1195
self.sendBadResponse(tag, "Server error encountered while deleting mailbox")
1198
self.sendPositiveResponse(tag, 'Mailbox deleted')
1200
auth_DELETE = (do_DELETE, arg_astring)
1201
select_DELETE = auth_DELETE
1203
def do_RENAME(self, tag, oldname, newname):
1204
oldname, newname = [self._parseMbox(n) for n in oldname, newname]
1205
if oldname.lower() == 'inbox' or newname.lower() == 'inbox':
1206
self.sendNegativeResponse(tag, 'You cannot rename the inbox, or rename another mailbox to inbox.')
1209
self.account.rename(oldname, newname)
1211
self.sendBadResponse(tag, 'Invalid command syntax')
1212
except MailboxException, m:
1213
self.sendNegativeResponse(tag, str(m))
1215
self.sendBadResponse(tag, "Server error encountered while renaming mailbox")
1218
self.sendPositiveResponse(tag, 'Mailbox renamed')
1220
auth_RENAME = (do_RENAME, arg_astring, arg_astring)
1221
select_RENAME = auth_RENAME
1223
def do_SUBSCRIBE(self, tag, name):
1224
name = self._parseMbox(name)
1226
self.account.subscribe(name)
1227
except MailboxException, m:
1228
self.sendNegativeResponse(tag, str(m))
1230
self.sendBadResponse(tag, "Server error encountered while subscribing to mailbox")
1233
self.sendPositiveResponse(tag, 'Subscribed')
1235
auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring)
1236
select_SUBSCRIBE = auth_SUBSCRIBE
1238
def do_UNSUBSCRIBE(self, tag, name):
1239
name = self._parseMbox(name)
1241
self.account.unsubscribe(name)
1242
except MailboxException, m:
1243
self.sendNegativeResponse(tag, str(m))
1245
self.sendBadResponse(tag, "Server error encountered while unsubscribing from mailbox")
1248
self.sendPositiveResponse(tag, 'Unsubscribed')
1250
auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring)
1251
select_UNSUBSCRIBE = auth_UNSUBSCRIBE
1253
def _listWork(self, tag, ref, mbox, sub, cmdName):
1254
mbox = self._parseMbox(mbox)
1255
maybeDeferred(self.account.listMailboxes, ref, mbox
1256
).addCallback(self._cbListWork, tag, sub, cmdName
1257
).addErrback(self._ebListWork, tag
1260
def _cbListWork(self, mailboxes, tag, sub, cmdName):
1261
for (name, box) in mailboxes:
1262
if not sub or self.account.isSubscribed(name):
1263
flags = box.getFlags()
1264
delim = box.getHierarchicalDelimiter()
1265
resp = (DontQuoteMe(cmdName), map(DontQuoteMe, flags), delim, name.encode('imap4-utf-7'))
1266
self.sendUntaggedResponse(collapseNestedLists(resp))
1267
self.sendPositiveResponse(tag, '%s completed' % (cmdName,))
1269
def _ebListWork(self, failure, tag):
1270
self.sendBadResponse(tag, "Server error encountered while listing mailboxes.")
1273
auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST')
1274
select_LIST = auth_LIST
1276
auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB')
1277
select_LSUB = auth_LSUB
1279
def do_STATUS(self, tag, mailbox, names):
1280
mailbox = self._parseMbox(mailbox)
1281
maybeDeferred(self.account.select, mailbox, 0
1282
).addCallback(self._cbStatusGotMailbox, tag, mailbox, names
1283
).addErrback(self._ebStatusGotMailbox, tag
1286
def _cbStatusGotMailbox(self, mbox, tag, mailbox, names):
1288
maybeDeferred(mbox.requestStatus, names).addCallbacks(
1289
self.__cbStatus, self.__ebStatus,
1290
(tag, mailbox), None, (tag, mailbox), None
1293
self.sendNegativeResponse(tag, "Could not open mailbox")
1295
def _ebStatusGotMailbox(self, failure, tag):
1296
self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
1299
auth_STATUS = (do_STATUS, arg_astring, arg_plist)
1300
select_STATUS = auth_STATUS
1302
def __cbStatus(self, status, tag, box):
1303
line = ' '.join(['%s %s' % x for x in status.iteritems()])
1304
self.sendUntaggedResponse('STATUS %s (%s)' % (box, line))
1305
self.sendPositiveResponse(tag, 'STATUS complete')
1307
def __ebStatus(self, failure, tag, box):
1308
self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, str(failure.value)))
1310
def do_APPEND(self, tag, mailbox, flags, date, message):
1311
mailbox = self._parseMbox(mailbox)
1312
maybeDeferred(self.account.select, mailbox
1313
).addCallback(self._cbAppendGotMailbox, tag, flags, date, message
1314
).addErrback(self._ebAppendGotMailbox, tag
1317
def _cbAppendGotMailbox(self, mbox, tag, flags, date, message):
1319
self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox')
1322
d = mbox.addMessage(message, flags, date)
1323
d.addCallback(self.__cbAppend, tag, mbox)
1324
d.addErrback(self.__ebAppend, tag)
1326
def _ebAppendGotMailbox(self, failure, tag):
1327
self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
1330
auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime,
1332
select_APPEND = auth_APPEND
1334
def __cbAppend(self, result, tag, mbox):
1335
self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount())
1336
self.sendPositiveResponse(tag, 'APPEND complete')
1338
def __ebAppend(self, failure, tag):
1339
self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value))
1341
def do_CHECK(self, tag):
1342
d = self.checkpoint()
1344
self.__cbCheck(None, tag)
1349
callbackArgs=(tag,),
1352
select_CHECK = (do_CHECK,)
1354
def __cbCheck(self, result, tag):
1355
self.sendPositiveResponse(tag, 'CHECK completed')
1357
def __ebCheck(self, failure, tag):
1358
self.sendBadResponse(tag, 'CHECK failed: ' + str(failure.value))
1360
def checkpoint(self):
1361
"""Called when the client issues a CHECK command.
1363
This should perform any checkpoint operations required by the server.
1364
It may be a long running operation, but may not block. If it returns
1365
a deferred, the client will only be informed of success (or failure)
1366
when the deferred's callback (or errback) is invoked.
1370
def do_CLOSE(self, tag):
1372
if self.mbox.isWriteable():
1373
d = maybeDeferred(self.mbox.expunge)
1374
cmbx = ICloseableMailbox(self.mbox, None)
1375
if cmbx is not None:
1377
d.addCallback(lambda result: cmbx.close())
1379
d = maybeDeferred(cmbx.close)
1381
d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,), None)
1383
self.__cbClose(None, tag)
1385
select_CLOSE = (do_CLOSE,)
1387
def __cbClose(self, result, tag):
1388
self.sendPositiveResponse(tag, 'CLOSE completed')
1389
self.mbox.removeListener(self)
1393
def __ebClose(self, failure, tag):
1394
self.sendBadResponse(tag, 'CLOSE failed: ' + str(failure.value))
1396
def do_EXPUNGE(self, tag):
1397
if self.mbox.isWriteable():
1398
maybeDeferred(self.mbox.expunge).addCallbacks(
1399
self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None
1402
self.sendNegativeResponse(tag, 'EXPUNGE ignored on read-only mailbox')
1404
select_EXPUNGE = (do_EXPUNGE,)
1406
def __cbExpunge(self, result, tag):
1408
self.sendUntaggedResponse('%d EXPUNGE' % e)
1409
self.sendPositiveResponse(tag, 'EXPUNGE completed')
1411
def __ebExpunge(self, failure, tag):
1412
self.sendBadResponse(tag, 'EXPUNGE failed: ' + str(failure.value))
1415
def do_SEARCH(self, tag, charset, query, uid=0):
1416
sm = ISearchableMailbox(self.mbox, None)
1418
maybeDeferred(sm.search, query, uid=uid).addCallbacks(
1419
self.__cbSearch, self.__ebSearch,
1420
(tag, self.mbox, uid), None, (tag,), None
1423
s = parseIdList('1:*')
1424
maybeDeferred(self.mbox.fetch, s, uid=uid).addCallbacks(
1425
self.__cbManualSearch, self.__ebSearch,
1426
(tag, self.mbox, query, uid), None, (tag,), None
1429
select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys)
1431
def __cbSearch(self, result, tag, mbox, uid):
1433
result = map(mbox.getUID, result)
1434
ids = ' '.join([str(i) for i in result])
1435
self.sendUntaggedResponse('SEARCH ' + ids)
1436
self.sendPositiveResponse(tag, 'SEARCH completed')
1438
def __cbManualSearch(self, result, tag, mbox, query, uid, searchResults = None):
1439
if searchResults is None:
1442
for (i, (id, msg)) in zip(range(5), result):
1443
if self.searchFilter(query, id, msg):
1445
searchResults.append(str(msg.getUID()))
1447
searchResults.append(str(id))
1449
from twisted.internet import reactor
1450
reactor.callLater(0, self.__cbManualSearch, result, tag, mbox, query, uid, searchResults)
1453
self.sendUntaggedResponse('SEARCH ' + ' '.join(searchResults))
1454
self.sendPositiveResponse(tag, 'SEARCH completed')
1456
def searchFilter(self, query, id, msg):
1458
if not self.singleSearchStep(query, id, msg):
1462
def singleSearchStep(self, query, id, msg):
1464
if isinstance(q, list):
1465
if not self.searchFilter(q, id, msg):
1469
f = getattr(self, 'search_' + c)
1471
if not f(query, id, msg):
1474
# IMAP goes *out of its way* to be complex
1475
# Sequence sets to search should be specified
1476
# with a command, like EVERYTHING ELSE.
1480
log.err('Unknown search term: ' + c)
1486
def search_ALL(self, query, id, msg):
1489
def search_ANSWERED(self, query, id, msg):
1490
return '\\Answered' in msg.getFlags()
1492
def search_BCC(self, query, id, msg):
1493
bcc = msg.getHeaders(False, 'bcc').get('bcc', '')
1494
return bcc.lower().find(query.pop(0).lower()) != -1
1496
def search_BEFORE(self, query, id, msg):
1497
date = parseTime(query.pop(0))
1498
return rfc822.parsedate(msg.getInternalDate()) < date
1500
def search_BODY(self, query, id, msg):
1501
body = query.pop(0).lower()
1502
return text.strFile(body, msg.getBodyFile(), False)
1504
def search_CC(self, query, id, msg):
1505
cc = msg.getHeaders(False, 'cc').get('cc', '')
1506
return cc.lower().find(query.pop(0).lower()) != -1
1508
def search_DELETED(self, query, id, msg):
1509
return '\\Deleted' in msg.getFlags()
1511
def search_DRAFT(self, query, id, msg):
1512
return '\\Draft' in msg.getFlags()
1514
def search_FLAGGED(self, query, id, msg):
1515
return '\\Flagged' in msg.getFlags()
1517
def search_FROM(self, query, id, msg):
1518
fm = msg.getHeaders(False, 'from').get('from', '')
1519
return fm.lower().find(query.pop(0).lower()) != -1
1521
def search_HEADER(self, query, id, msg):
1522
hdr = query.pop(0).lower()
1523
hdr = msg.getHeaders(False, hdr).get(hdr, '')
1524
return hdr.lower().find(query.pop(0).lower()) != -1
1526
def search_KEYWORD(self, query, id, msg):
1530
def search_LARGER(self, query, id, msg):
1531
return int(query.pop(0)) < msg.getSize()
1533
def search_NEW(self, query, id, msg):
1534
return '\\Recent' in msg.getFlags() and '\\Seen' not in msg.getFlags()
1536
def search_NOT(self, query, id, msg):
1537
return not self.singleSearchStep(query, id, msg)
1539
def search_OLD(self, query, id, msg):
1540
return '\\Recent' not in msg.getFlags()
1542
def search_ON(self, query, id, msg):
1543
date = parseTime(query.pop(0))
1544
return rfc822.parsedate(msg.getInternalDate()) == date
1546
def search_OR(self, query, id, msg):
1547
a = self.singleSearchStep(query, id, msg)
1548
b = self.singleSearchStep(query, id, msg)
1551
def search_RECENT(self, query, id, msg):
1552
return '\\Recent' in msg.getFlags()
1554
def search_SEEN(self, query, id, msg):
1555
return '\\Seen' in msg.getFlags()
1557
def search_SENTBEFORE(self, query, id, msg):
1558
date = msg.getHeader(False, 'date').get('date', '')
1559
date = rfc822.parsedate(date)
1560
return date < parseTime(query.pop(0))
1562
def search_SENTON(self, query, id, msg):
1563
date = msg.getHeader(False, 'date').get('date', '')
1564
date = rfc822.parsedate(date)
1565
return date[:3] == parseTime(query.pop(0))[:3]
1567
def search_SENTSINCE(self, query, id, msg):
1568
date = msg.getHeader(False, 'date').get('date', '')
1569
date = rfc822.parsedate(date)
1570
return date > parseTime(query.pop(0))
1572
def search_SINCE(self, query, id, msg):
1573
date = parseTime(query.pop(0))
1574
return rfc822.parsedate(msg.getInternalDate()) > date
1576
def search_SMALLER(self, query, id, msg):
1577
return int(query.pop(0)) > msg.getSize()
1579
def search_SUBJECT(self, query, id, msg):
1580
subj = msg.getHeaders(False, 'subject').get('subject', '')
1581
return subj.lower().find(query.pop(0).lower()) != -1
1583
def search_TEXT(self, query, id, msg):
1584
# XXX - This must search headers too
1585
body = query.pop(0).lower()
1586
return text.strFile(body, msg.getBodyFile(), False)
1588
def search_TO(self, query, id, msg):
1589
to = msg.getHeaders(False, 'to').get('to', '')
1590
return to.lower().find(query.pop(0).lower()) != -1
1592
def search_UID(self, query, id, msg):
1595
return msg.getUID() in m
1597
def search_UNANSWERED(self, query, id, msg):
1598
return '\\Answered' not in msg.getFlags()
1600
def search_UNDELETED(self, query, id, msg):
1601
return '\\Deleted' not in msg.getFlags()
1603
def search_UNDRAFT(self, query, id, msg):
1604
return '\\Draft' not in msg.getFlags()
1606
def search_UNFLAGGED(self, query, id, msg):
1607
return '\\Flagged' not in msg.getFlags()
1609
def search_UNKEYWORD(self, query, id, msg):
1613
def search_UNSEEN(self, query, id, msg):
1614
return '\\Seen' not in msg.getFlags()
1616
def __ebSearch(self, failure, tag):
1617
self.sendBadResponse(tag, 'SEARCH failed: ' + str(failure.value))
1620
def do_FETCH(self, tag, messages, query, uid=0):
1622
maybeDeferred(self.mbox.fetch, messages, uid=uid
1624
).addCallback(self.__cbFetch, tag, query, uid
1625
).addErrback(self.__ebFetch, tag
1628
self.sendPositiveResponse(tag, 'FETCH complete')
1630
select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt)
1632
def __cbFetch(self, results, tag, query, uid):
1633
if self.blocked is None:
1635
self._oldTimeout = self.setTimeout(None)
1637
id, msg = results.next()
1638
except StopIteration:
1639
# All results have been processed, deliver completion notification.
1640
self.sendPositiveResponse(tag, 'FETCH completed')
1642
# The idle timeout was suspended while we delivered results,
1644
self.setTimeout(self._oldTimeout)
1645
del self._oldTimeout
1647
# Instance state is now consistent again (ie, it is as though
1648
# the fetch command never ran), so allow any pending blocked
1649
# commands to execute.
1652
self.spewMessage(id, msg, query, uid
1653
).addCallback(lambda _: self.__cbFetch(results, tag, query, uid)
1654
).addErrback(self.__ebSpewMessage
1657
def __ebSpewMessage(self, failure):
1658
# This indicates a programming error.
1659
# There's no reliable way to indicate anything to the client, since we
1660
# may have already written an arbitrary amount of data in response to
1663
self.transport.loseConnection()
1665
def spew_envelope(self, id, msg, _w=None, _f=None):
1667
_w = self.transport.write
1668
_w('ENVELOPE ' + collapseNestedLists([getEnvelope(msg)]))
1670
def spew_flags(self, id, msg, _w=None, _f=None):
1672
_w = self.transport.write
1673
_w('FLAGS ' + '(%s)' % (' '.join(msg.getFlags())))
1675
def spew_internaldate(self, id, msg, _w=None, _f=None):
1677
_w = self.transport.write
1678
idate = msg.getInternalDate()
1679
ttup = rfc822.parsedate_tz(idate)
1681
log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate))
1682
raise IMAP4Exception("Internal failure generating INTERNALDATE")
1684
odate = time.strftime("%d-%b-%Y %H:%M:%S ", ttup[:9])
1686
odate = odate + "+0000"
1692
odate = odate + sign + string.zfill(str(((abs(ttup[9]) / 3600) * 100 + (abs(ttup[9]) % 3600) / 60)), 4)
1693
_w('INTERNALDATE ' + _quote(odate))
1695
def spew_rfc822header(self, id, msg, _w=None, _f=None):
1697
_w = self.transport.write
1698
hdrs = _formatHeaders(msg.getHeaders(True))
1699
_w('RFC822.HEADER ' + _literal(hdrs))
1701
def spew_rfc822text(self, id, msg, _w=None, _f=None):
1703
_w = self.transport.write
1706
return FileProducer(msg.getBodyFile()
1707
).beginProducing(self.transport
1710
def spew_rfc822size(self, id, msg, _w=None, _f=None):
1712
_w = self.transport.write
1713
_w('RFC822.SIZE ' + str(msg.getSize()))
1715
def spew_rfc822(self, id, msg, _w=None, _f=None):
1717
_w = self.transport.write
1720
mf = IMessageFile(msg, None)
1722
return FileProducer(mf.open()
1723
).beginProducing(self.transport
1725
return MessageProducer(msg, None, self._scheduler
1726
).beginProducing(self.transport
1729
def spew_uid(self, id, msg, _w=None, _f=None):
1731
_w = self.transport.write
1732
_w('UID ' + str(msg.getUID()))
1734
def spew_bodystructure(self, id, msg, _w=None, _f=None):
1735
_w('BODYSTRUCTURE ' + collapseNestedLists([getBodyStructure(msg, True)]))
1737
def spew_body(self, part, id, msg, _w=None, _f=None):
1739
_w = self.transport.write
1741
if msg.isMultipart():
1742
msg = msg.getSubPart(p)
1744
# Non-multipart messages have an implicit first part but no
1745
# other parts - reject any request for any other part.
1746
raise TypeError("Requested subpart of non-multipart message")
1749
hdrs = msg.getHeaders(part.header.negate, *part.header.fields)
1750
hdrs = _formatHeaders(hdrs)
1751
_w(str(part) + ' ' + _literal(hdrs))
1755
return FileProducer(msg.getBodyFile()
1756
).beginProducing(self.transport
1759
hdrs = _formatHeaders(msg.getHeaders(True))
1760
_w(str(part) + ' ' + _literal(hdrs))
1765
return FileProducer(msg.getBodyFile()
1766
).beginProducing(self.transport
1769
mf = IMessageFile(msg, None)
1771
return FileProducer(mf.open()).beginProducing(self.transport)
1772
return MessageProducer(msg, None, self._scheduler).beginProducing(self.transport)
1775
_w('BODY ' + collapseNestedLists([getBodyStructure(msg)]))
1777
def spewMessage(self, id, msg, query, uid):
1778
wbuf = WriteBuffer(self.transport)
1782
write('* %d FETCH (' % (id,))
1792
if part.type == 'uid':
1794
if part.type == 'body':
1795
yield self.spew_body(part, id, msg, write, flush)
1797
f = getattr(self, 'spew_' + part.type)
1798
yield f(id, msg, write, flush)
1799
if part is not query[-1]:
1801
if uid and not seenUID:
1803
yield self.spew_uid(id, msg, write, flush)
1806
return self._scheduler(spew())
1808
def __ebFetch(self, failure, tag):
1810
self.sendBadResponse(tag, 'FETCH failed: ' + str(failure.value))
1812
def do_STORE(self, tag, messages, mode, flags, uid=0):
1814
silent = mode.endswith('SILENT')
1815
if mode.startswith('+'):
1817
elif mode.startswith('-'):
1822
maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallbacks(
1823
self.__cbStore, self.__ebStore, (tag, self.mbox, uid, silent), None, (tag,), None
1826
select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist)
1828
def __cbStore(self, result, tag, mbox, uid, silent):
1829
if result and not silent:
1830
for (k, v) in result.iteritems():
1832
uidstr = ' UID %d' % mbox.getUID(k)
1835
self.sendUntaggedResponse('%d FETCH (FLAGS (%s)%s)' %
1836
(k, ' '.join(v), uidstr))
1837
self.sendPositiveResponse(tag, 'STORE completed')
1839
def __ebStore(self, failure, tag):
1840
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1842
def do_COPY(self, tag, messages, mailbox, uid=0):
1843
mailbox = self._parseMbox(mailbox)
1844
maybeDeferred(self.account.select, mailbox
1845
).addCallback(self._cbCopySelectedMailbox, tag, messages, mailbox, uid
1846
).addErrback(self._ebCopySelectedMailbox, tag
1848
select_COPY = (do_COPY, arg_seqset, arg_astring)
1850
def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid):
1852
self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox)
1854
maybeDeferred(self.mbox.fetch, messages, uid
1855
).addCallback(self.__cbCopy, tag, mbox
1856
).addCallback(self.__cbCopied, tag, mbox
1857
).addErrback(self.__ebCopy, tag
1860
def _ebCopySelectedMailbox(self, failure, tag):
1861
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1863
def __cbCopy(self, messages, tag, mbox):
1864
# XXX - This should handle failures with a rollback or something
1869
fastCopyMbox = IMessageCopier(mbox, None)
1870
for (id, msg) in messages:
1871
if fastCopyMbox is not None:
1872
d = maybeDeferred(fastCopyMbox.copy, msg)
1873
addedDeferreds.append(d)
1876
# XXX - The following should be an implementation of IMessageCopier.copy
1877
# on an IMailbox->IMessageCopier adapter.
1879
flags = msg.getFlags()
1880
date = msg.getInternalDate()
1882
body = IMessageFile(msg, None)
1883
if body is not None:
1884
bodyFile = body.open()
1885
d = maybeDeferred(mbox.addMessage, bodyFile, flags, date)
1890
buffer = tempfile.TemporaryFile()
1891
d = MessageProducer(msg, buffer, self._scheduler
1892
).beginProducing(None
1893
).addCallback(lambda _, b=buffer, f=flags, d=date: mbox.addMessage(rewind(b), f, d)
1895
addedDeferreds.append(d)
1896
return defer.DeferredList(addedDeferreds)
1898
def __cbCopied(self, deferredIds, tag, mbox):
1901
for (status, result) in deferredIds:
1905
failures.append(result.value)
1907
self.sendNegativeResponse(tag, '[ALERT] Some messages were not copied')
1909
self.sendPositiveResponse(tag, 'COPY completed')
1911
def __ebCopy(self, failure, tag):
1912
self.sendBadResponse(tag, 'COPY failed:' + str(failure.value))
1915
def do_UID(self, tag, command, line):
1916
command = command.upper()
1918
if command not in ('COPY', 'FETCH', 'STORE', 'SEARCH'):
1919
raise IllegalClientResponse(command)
1921
self.dispatchCommand(tag, command, line, uid=1)
1923
select_UID = (do_UID, arg_atom, arg_line)
1925
# IMailboxListener implementation
1927
def modeChanged(self, writeable):
1929
self.sendUntaggedResponse(message='[READ-WRITE]', async=True)
1931
self.sendUntaggedResponse(message='[READ-ONLY]', async=True)
1933
def flagsChanged(self, newFlags):
1934
for (mId, flags) in newFlags.iteritems():
1935
msg = '%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags))
1936
self.sendUntaggedResponse(msg, async=True)
1938
def newMessages(self, exists, recent):
1939
if exists is not None:
1940
self.sendUntaggedResponse('%d EXISTS' % exists, async=True)
1941
if recent is not None:
1942
self.sendUntaggedResponse('%d RECENT' % recent, async=True)
1945
class UnhandledResponse(IMAP4Exception): pass
1947
class NegativeResponse(IMAP4Exception): pass
1949
class NoSupportedAuthentication(IMAP4Exception):
1950
def __init__(self, serverSupports, clientSupports):
1951
IMAP4Exception.__init__(self, 'No supported authentication schemes available')
1952
self.serverSupports = serverSupports
1953
self.clientSupports = clientSupports
1956
return (IMAP4Exception.__str__(self)
1957
+ ': Server supports %r, client supports %r'
1958
% (self.serverSupports, self.clientSupports))
1960
class IllegalServerResponse(IMAP4Exception): pass
1962
TIMEOUT_ERROR = error.TimeoutError()
1964
class IMAP4Client(basic.LineReceiver, policies.TimeoutMixin):
1965
"""IMAP4 client protocol implementation
1967
@ivar state: A string representing the state the connection is currently
1970
implements(IMailboxListener)
1980
# Number of seconds to wait before timing out a connection.
1981
# If the number is <= 0 no timeout checking will be performed.
1984
# Capabilities are not allowed to change during the session
1985
# So cache the first response and use that for all later
1989
_memoryFileLimit = 1024 * 1024 * 10
1991
# Authentication is pluggable. This maps names to IClientAuthentication
1993
authenticators = None
1995
STATUS_CODES = ('OK', 'NO', 'BAD', 'PREAUTH', 'BYE')
1997
STATUS_TRANSFORMATIONS = {
1998
'MESSAGES': int, 'RECENT': int, 'UNSEEN': int
2003
def __init__(self, contextFactory = None):
2006
self.authenticators = {}
2007
self.context = contextFactory
2011
self._lastCmd = None
2013
def registerAuthenticator(self, auth):
2014
"""Register a new form of authentication
2016
When invoking the authenticate() method of IMAP4Client, the first
2017
matching authentication scheme found will be used. The ordering is
2018
that in which the server lists support authentication schemes.
2020
@type auth: Implementor of C{IClientAuthentication}
2021
@param auth: The object to use to perform the client
2022
side of this authentication scheme.
2024
self.authenticators[auth.getName().upper()] = auth
2026
def rawDataReceived(self, data):
2027
if self.timeout > 0:
2030
self._pendingSize -= len(data)
2031
if self._pendingSize > 0:
2032
self._pendingBuffer.write(data)
2035
if self._pendingSize < 0:
2036
data, passon = data[:self._pendingSize], data[self._pendingSize:]
2037
self._pendingBuffer.write(data)
2038
rest = self._pendingBuffer
2039
self._pendingBuffer = None
2040
self._pendingSize = None
2042
self._parts.append(rest.read())
2043
self.setLineMode(passon.lstrip('\r\n'))
2045
# def sendLine(self, line):
2046
# print 'S:', repr(line)
2047
# return basic.LineReceiver.sendLine(self, line)
2049
def _setupForLiteral(self, rest, octets):
2050
self._pendingBuffer = self.messageFile(octets)
2051
self._pendingSize = octets
2052
if self._parts is None:
2053
self._parts = [rest, '\r\n']
2055
self._parts.extend([rest, '\r\n'])
2058
def connectionMade(self):
2059
if self.timeout > 0:
2060
self.setTimeout(self.timeout)
2062
def connectionLost(self, reason):
2063
"""We are no longer connected"""
2064
if self.timeout > 0:
2065
self.setTimeout(None)
2066
if self.queued is not None:
2067
queued = self.queued
2070
cmd.defer.errback(reason)
2071
if self.tags is not None:
2074
for cmd in tags.itervalues():
2075
if cmd is not None and cmd.defer is not None:
2076
cmd.defer.errback(reason)
2079
def lineReceived(self, line):
2080
# print 'C: ' + repr(line)
2081
if self.timeout > 0:
2084
lastPart = line.rfind(' ')
2086
lastPart = line[lastPart + 1:]
2087
if lastPart.startswith('{') and lastPart.endswith('}'):
2088
# It's a literal a-comin' in
2090
octets = int(lastPart[1:-1])
2092
raise IllegalServerResponse(line)
2093
if self._parts is None:
2094
self._tag, parts = line.split(None, 1)
2097
self._setupForLiteral(parts, octets)
2100
if self._parts is None:
2101
# It isn't a literal at all
2102
self._regularDispatch(line)
2104
# If an expression is in progress, no tag is required here
2105
# Since we didn't find a literal indicator, this expression
2107
self._parts.append(line)
2108
tag, rest = self._tag, ''.join(self._parts)
2109
self._tag = self._parts = None
2110
self.dispatchCommand(tag, rest)
2112
def timeoutConnection(self):
2113
if self._lastCmd and self._lastCmd.defer is not None:
2114
d, self._lastCmd.defer = self._lastCmd.defer, None
2115
d.errback(TIMEOUT_ERROR)
2118
for cmd in self.queued:
2119
if cmd.defer is not None:
2120
d, cmd.defer = cmd.defer, d
2121
d.errback(TIMEOUT_ERROR)
2123
self.transport.loseConnection()
2125
def _regularDispatch(self, line):
2126
parts = line.split(None, 1)
2130
self.dispatchCommand(tag, rest)
2132
def messageFile(self, octets):
2133
"""Create a file to which an incoming message may be written.
2135
@type octets: C{int}
2136
@param octets: The number of octets which will be written to the file
2138
@rtype: Any object which implements C{write(string)} and
2140
@return: A file-like object
2142
if octets > self._memoryFileLimit:
2143
return tempfile.TemporaryFile()
2145
return StringIO.StringIO()
2148
tag = '%0.4X' % self.tagID
2152
def dispatchCommand(self, tag, rest):
2153
if self.state is None:
2154
f = self.response_UNAUTH
2156
f = getattr(self, 'response_' + self.state.upper(), None)
2162
self.transport.loseConnection()
2164
log.err("Cannot dispatch: %s, %s, %s" % (self.state, tag, rest))
2165
self.transport.loseConnection()
2167
def response_UNAUTH(self, tag, rest):
2168
if self.state is None:
2169
# Server greeting, this is
2170
status, rest = rest.split(None, 1)
2171
if status.upper() == 'OK':
2172
self.state = 'unauth'
2173
elif status.upper() == 'PREAUTH':
2176
# XXX - This is rude.
2177
self.transport.loseConnection()
2178
raise IllegalServerResponse(tag + ' ' + rest)
2180
b, e = rest.find('['), rest.find(']')
2181
if b != -1 and e != -1:
2182
self.serverGreeting(self.__cbCapabilities(([rest[b:e]], None)))
2184
self.serverGreeting(None)
2186
self._defaultHandler(tag, rest)
2188
def response_AUTH(self, tag, rest):
2189
self._defaultHandler(tag, rest)
2191
def _defaultHandler(self, tag, rest):
2192
if tag == '*' or tag == '+':
2193
if not self.waiting:
2194
self._extraInfo([rest])
2196
cmd = self.tags[self.waiting]
2198
cmd.continuation(rest)
2200
cmd.lines.append(rest)
2203
cmd = self.tags[tag]
2205
# XXX - This is rude.
2206
self.transport.loseConnection()
2207
raise IllegalServerResponse(tag + ' ' + rest)
2209
status, line = rest.split(None, 1)
2211
# Give them this last line, too
2212
cmd.finish(rest, self._extraInfo)
2214
cmd.defer.errback(IMAP4Exception(line))
2219
def _flushQueue(self):
2221
cmd = self.queued.pop(0)
2224
self.sendLine(cmd.format(t))
2227
def _extraInfo(self, lines):
2228
# XXX - This is terrible.
2229
# XXX - Also, this should collapse temporally proximate calls into single
2230
# invocations of IMailboxListener methods, where possible.
2232
recent = exists = None
2234
if L.find('EXISTS') != -1:
2235
exists = int(L.split()[0])
2236
elif L.find('RECENT') != -1:
2237
recent = int(L.split()[0])
2238
elif L.find('READ-ONLY') != -1:
2240
elif L.find('READ-WRITE') != -1:
2242
elif L.find('FETCH') != -1:
2243
for (mId, fetched) in self.__cbFetch(([L], None)).iteritems():
2245
for f in fetched.get('FLAGS', []):
2247
flags.setdefault(mId, []).extend(sum)
2249
log.msg('Unhandled unsolicited response: ' + repr(L))
2251
self.flagsChanged(flags)
2252
if recent is not None or exists is not None:
2253
self.newMessages(exists, recent)
2255
def sendCommand(self, cmd):
2256
cmd.defer = defer.Deferred()
2258
self.queued.append(cmd)
2262
self.sendLine(cmd.format(t))
2267
def getCapabilities(self, useCache=1):
2268
"""Request the capabilities available on this server.
2270
This command is allowed in any state of connection.
2272
@type useCache: C{bool}
2273
@param useCache: Specify whether to use the capability-cache or to
2274
re-retrieve the capabilities from the server. Server capabilities
2275
should never change, so for normal use, this flag should never be
2279
@return: A deferred whose callback will be invoked with a
2280
dictionary mapping capability types to lists of supported
2281
mechanisms, or to None if a support list is not applicable.
2283
if useCache and self._capCache is not None:
2284
return defer.succeed(self._capCache)
2286
resp = ('CAPABILITY',)
2287
d = self.sendCommand(Command(cmd, wantResponse=resp))
2288
d.addCallback(self.__cbCapabilities)
2291
def __cbCapabilities(self, (lines, tagline)):
2294
rest = rest.split()[1:]
2300
caps.setdefault(cap[:eq], []).append(cap[eq+1:])
2301
self._capCache = caps
2305
"""Inform the server that we are done with the connection.
2307
This command is allowed in any state of connection.
2310
@return: A deferred whose callback will be invoked with None
2311
when the proper server acknowledgement has been received.
2313
d = self.sendCommand(Command('LOGOUT', wantResponse=('BYE',)))
2314
d.addCallback(self.__cbLogout)
2317
def __cbLogout(self, (lines, tagline)):
2318
self.transport.loseConnection()
2319
# We don't particularly care what the server said
2324
"""Perform no operation.
2326
This command is allowed in any state of connection.
2329
@return: A deferred whose callback will be invoked with a list
2330
of untagged status updates the server responds with.
2332
d = self.sendCommand(Command('NOOP'))
2333
d.addCallback(self.__cbNoop)
2336
def __cbNoop(self, (lines, tagline)):
2337
# Conceivable, this is elidable.
2338
# It is, afterall, a no-op.
2341
def startTLS(self, contextFactory=None):
2343
Initiates a 'STARTTLS' request and negotiates the TLS / SSL
2346
@param contextFactory: The TLS / SSL Context Factory to
2347
leverage. If the contextFactory is None the IMAP4Client will
2348
either use the current TLS / SSL Context Factory or attempt to
2351
@type contextFactory: C{ssl.ClientContextFactory}
2353
@return: A Deferred which fires when the transport has been
2354
secured according to the given contextFactory, or which fails
2355
if the transport cannot be secured.
2357
assert not self.startedTLS, "Client and Server are currently communicating via TLS"
2359
if contextFactory is None:
2360
contextFactory = self._getContextFactory()
2362
if contextFactory is None:
2363
return defer.fail(IMAP4Exception(
2364
"IMAP4Client requires a TLS context to "
2365
"initiate the STARTTLS handshake"))
2367
if 'STARTTLS' not in self._capCache:
2368
return defer.fail(IMAP4Exception(
2369
"Server does not support secure communication "
2372
tls = interfaces.ITLSTransport(self.transport, None)
2374
return defer.fail(IMAP4Exception(
2375
"IMAP4Client transport does not implement "
2376
"interfaces.ITLSTransport"))
2378
d = self.sendCommand(Command('STARTTLS'))
2379
d.addCallback(self._startedTLS, contextFactory)
2380
d.addCallback(lambda _: self.getCapabilities())
2384
def authenticate(self, secret):
2385
"""Attempt to enter the authenticated state with the server
2387
This command is allowed in the Non-Authenticated state.
2390
@return: A deferred whose callback is invoked if the authentication
2391
succeeds and whose errback will be invoked otherwise.
2393
if self._capCache is None:
2394
d = self.getCapabilities()
2396
d = defer.succeed(self._capCache)
2397
d.addCallback(self.__cbAuthenticate, secret)
2400
def __cbAuthenticate(self, caps, secret):
2401
auths = caps.get('AUTH', ())
2402
for scheme in auths:
2403
if scheme.upper() in self.authenticators:
2404
cmd = Command('AUTHENTICATE', scheme, (),
2405
self.__cbContinueAuth, scheme,
2407
return self.sendCommand(cmd)
2410
return defer.fail(NoSupportedAuthentication(
2411
auths, self.authenticators.keys()))
2413
def ebStartTLS(err):
2414
err.trap(IMAP4Exception)
2415
# We couldn't negotiate TLS for some reason
2416
return defer.fail(NoSupportedAuthentication(
2417
auths, self.authenticators.keys()))
2420
d.addErrback(ebStartTLS)
2421
d.addCallback(lambda _: self.getCapabilities())
2422
d.addCallback(self.__cbAuthTLS, secret)
2426
def __cbContinueAuth(self, rest, scheme, secret):
2428
chal = base64.decodestring(rest + '\n')
2429
except binascii.Error:
2431
raise IllegalServerResponse(rest)
2432
self.transport.loseConnection()
2434
auth = self.authenticators[scheme]
2435
chal = auth.challengeResponse(secret, chal)
2436
self.sendLine(base64.encodestring(chal).strip())
2438
def __cbAuthTLS(self, caps, secret):
2439
auths = caps.get('AUTH', ())
2440
for scheme in auths:
2441
if scheme.upper() in self.authenticators:
2442
cmd = Command('AUTHENTICATE', scheme, (),
2443
self.__cbContinueAuth, scheme,
2445
return self.sendCommand(cmd)
2446
raise NoSupportedAuthentication(auths, self.authenticators.keys())
2449
def login(self, username, password):
2450
"""Authenticate with the server using a username and password
2452
This command is allowed in the Non-Authenticated state. If the
2453
server supports the STARTTLS capability and our transport supports
2454
TLS, TLS is negotiated before the login command is issued.
2456
A more secure way to log in is to use C{startTLS} or
2457
C{authenticate} or both.
2459
@type username: C{str}
2460
@param username: The username to log in with
2462
@type password: C{str}
2463
@param password: The password to log in with
2466
@return: A deferred whose callback is invoked if login is successful
2467
and whose errback is invoked otherwise.
2469
d = maybeDeferred(self.getCapabilities)
2470
d.addCallback(self.__cbLoginCaps, username, password)
2473
def serverGreeting(self, caps):
2474
"""Called when the server has sent us a greeting.
2477
@param caps: Capabilities the server advertised in its greeting.
2480
def _getContextFactory(self):
2481
if self.context is not None:
2484
from twisted.internet import ssl
2488
context = ssl.ClientContextFactory()
2489
context.method = ssl.SSL.TLSv1_METHOD
2492
def __cbLoginCaps(self, capabilities, username, password):
2493
# If the server advertises STARTTLS, we might want to try to switch to TLS
2494
tryTLS = 'STARTTLS' in capabilities
2496
# If our transport supports switching to TLS, we might want to try to switch to TLS.
2497
tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None
2499
# If our transport is not already using TLS, we might want to try to switch to TLS.
2500
nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None
2502
if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport:
2508
callbackArgs=(username, password),
2513
log.msg("Server has no TLS support. logging in over cleartext!")
2514
args = ' '.join((_quote(username), _quote(password)))
2515
return self.sendCommand(Command('LOGIN', args))
2517
def _startedTLS(self, result, context):
2518
self.transport.startTLS(context)
2519
self._capCache = None
2520
self.startedTLS = True
2523
def __cbLoginTLS(self, result, username, password):
2524
args = ' '.join((_quote(username), _quote(password)))
2525
return self.sendCommand(Command('LOGIN', args))
2527
def __ebLoginTLS(self, failure):
2531
def namespace(self):
2532
"""Retrieve information about the namespaces available to this account
2534
This command is allowed in the Authenticated and Selected states.
2537
@return: A deferred whose callback is invoked with namespace
2538
information. An example of this information is::
2540
[[['', '/']], [], []]
2542
which indicates a single personal namespace called '' with '/'
2543
as its hierarchical delimiter, and no shared or user namespaces.
2546
resp = ('NAMESPACE',)
2547
d = self.sendCommand(Command(cmd, wantResponse=resp))
2548
d.addCallback(self.__cbNamespace)
2551
def __cbNamespace(self, (lines, last)):
2553
parts = line.split(None, 1)
2555
if parts[0] == 'NAMESPACE':
2556
# XXX UGGG parsing hack :(
2557
r = parseNestedParens('(' + parts[1] + ')')[0]
2558
return [e or [] for e in r]
2559
log.err("No NAMESPACE response to NAMESPACE command")
2562
def select(self, mailbox):
2565
This command is allowed in the Authenticated and Selected states.
2567
@type mailbox: C{str}
2568
@param mailbox: The name of the mailbox to select
2571
@return: A deferred whose callback is invoked with mailbox
2572
information if the select is successful and whose errback is
2573
invoked otherwise. Mailbox information consists of a dictionary
2574
with the following keys and values::
2576
FLAGS: A list of strings containing the flags settable on
2577
messages in this mailbox.
2579
EXISTS: An integer indicating the number of messages in this
2582
RECENT: An integer indicating the number of \"recent\"
2583
messages in this mailbox.
2585
UNSEEN: An integer indicating the number of messages not
2586
flagged \\Seen in this mailbox.
2588
PERMANENTFLAGS: A list of strings containing the flags that
2589
can be permanently set on messages in this mailbox.
2591
UIDVALIDITY: An integer uniquely identifying this mailbox.
2594
args = _prepareMailboxName(mailbox)
2595
resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
2596
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2597
d.addCallback(self.__cbSelect, 1)
2600
def examine(self, mailbox):
2601
"""Select a mailbox in read-only mode
2603
This command is allowed in the Authenticated and Selected states.
2605
@type mailbox: C{str}
2606
@param mailbox: The name of the mailbox to examine
2609
@return: A deferred whose callback is invoked with mailbox
2610
information if the examine is successful and whose errback
2611
is invoked otherwise. Mailbox information consists of a dictionary
2612
with the following keys and values::
2614
'FLAGS': A list of strings containing the flags settable on
2615
messages in this mailbox.
2617
'EXISTS': An integer indicating the number of messages in this
2620
'RECENT': An integer indicating the number of \"recent\"
2621
messages in this mailbox.
2623
'UNSEEN': An integer indicating the number of messages not
2624
flagged \\Seen in this mailbox.
2626
'PERMANENTFLAGS': A list of strings containing the flags that
2627
can be permanently set on messages in this mailbox.
2629
'UIDVALIDITY': An integer uniquely identifying this mailbox.
2632
args = _prepareMailboxName(mailbox)
2633
resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
2634
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2635
d.addCallback(self.__cbSelect, 0)
2638
def __cbSelect(self, (lines, tagline), rw):
2639
# In the absense of specification, we are free to assume:
2641
datum = {'READ-WRITE': rw}
2642
lines.append(tagline)
2644
split = parts.split()
2646
if split[1].upper().strip() == 'EXISTS':
2648
datum['EXISTS'] = int(split[0])
2650
raise IllegalServerResponse(parts)
2651
elif split[1].upper().strip() == 'RECENT':
2653
datum['RECENT'] = int(split[0])
2655
raise IllegalServerResponse(parts)
2657
log.err('Unhandled SELECT response (1): ' + parts)
2658
elif split[0].upper().strip() == 'FLAGS':
2659
split = parts.split(None, 1)
2660
datum['FLAGS'] = tuple(parseNestedParens(split[1])[0])
2661
elif split[0].upper().strip() == 'OK':
2662
begin = parts.find('[')
2663
end = parts.find(']')
2664
if begin == -1 or end == -1:
2665
raise IllegalServerResponse(parts)
2667
content = parts[begin+1:end].split(None, 1)
2668
if len(content) >= 1:
2669
key = content[0].upper()
2670
if key == 'READ-ONLY':
2671
datum['READ-WRITE'] = 0
2672
elif key == 'READ-WRITE':
2673
datum['READ-WRITE'] = 1
2674
elif key == 'UIDVALIDITY':
2676
datum['UIDVALIDITY'] = int(content[1])
2678
raise IllegalServerResponse(parts)
2679
elif key == 'UNSEEN':
2681
datum['UNSEEN'] = int(content[1])
2683
raise IllegalServerResponse(parts)
2684
elif key == 'UIDNEXT':
2685
datum['UIDNEXT'] = int(content[1])
2686
elif key == 'PERMANENTFLAGS':
2687
datum['PERMANENTFLAGS'] = tuple(parseNestedParens(content[1])[0])
2689
log.err('Unhandled SELECT response (2): ' + parts)
2691
log.err('Unhandled SELECT response (3): ' + parts)
2693
log.err('Unhandled SELECT response (4): ' + parts)
2696
def create(self, name):
2697
"""Create a new mailbox on the server
2699
This command is allowed in the Authenticated and Selected states.
2702
@param name: The name of the mailbox to create.
2705
@return: A deferred whose callback is invoked if the mailbox creation
2706
is successful and whose errback is invoked otherwise.
2708
return self.sendCommand(Command('CREATE', _prepareMailboxName(name)))
2710
def delete(self, name):
2713
This command is allowed in the Authenticated and Selected states.
2716
@param name: The name of the mailbox to delete.
2719
@return: A deferred whose calblack is invoked if the mailbox is
2720
deleted successfully and whose errback is invoked otherwise.
2722
return self.sendCommand(Command('DELETE', _prepareMailboxName(name)))
2724
def rename(self, oldname, newname):
2727
This command is allowed in the Authenticated and Selected states.
2729
@type oldname: C{str}
2730
@param oldname: The current name of the mailbox to rename.
2732
@type newname: C{str}
2733
@param newname: The new name to give the mailbox.
2736
@return: A deferred whose callback is invoked if the rename is
2737
successful and whose errback is invoked otherwise.
2739
oldname = _prepareMailboxName(oldname)
2740
newname = _prepareMailboxName(newname)
2741
return self.sendCommand(Command('RENAME', ' '.join((oldname, newname))))
2743
def subscribe(self, name):
2744
"""Add a mailbox to the subscription list
2746
This command is allowed in the Authenticated and Selected states.
2749
@param name: The mailbox to mark as 'active' or 'subscribed'
2752
@return: A deferred whose callback is invoked if the subscription
2753
is successful and whose errback is invoked otherwise.
2755
return self.sendCommand(Command('SUBSCRIBE', _prepareMailboxName(name)))
2757
def unsubscribe(self, name):
2758
"""Remove a mailbox from the subscription list
2760
This command is allowed in the Authenticated and Selected states.
2763
@param name: The mailbox to unsubscribe
2766
@return: A deferred whose callback is invoked if the unsubscription
2767
is successful and whose errback is invoked otherwise.
2769
return self.sendCommand(Command('UNSUBSCRIBE', _prepareMailboxName(name)))
2771
def list(self, reference, wildcard):
2772
"""List a subset of the available mailboxes
2774
This command is allowed in the Authenticated and Selected states.
2776
@type reference: C{str}
2777
@param reference: The context in which to interpret C{wildcard}
2779
@type wildcard: C{str}
2780
@param wildcard: The pattern of mailbox names to match, optionally
2781
including either or both of the '*' and '%' wildcards. '*' will
2782
match zero or more characters and cross hierarchical boundaries.
2783
'%' will also match zero or more characters, but is limited to a
2784
single hierarchical level.
2787
@return: A deferred whose callback is invoked with a list of C{tuple}s,
2788
the first element of which is a C{tuple} of mailbox flags, the second
2789
element of which is the hierarchy delimiter for this mailbox, and the
2790
third of which is the mailbox name; if the command is unsuccessful,
2791
the deferred's errback is invoked instead.
2794
args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
2796
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2797
d.addCallback(self.__cbList, 'LIST')
2800
def lsub(self, reference, wildcard):
2801
"""List a subset of the subscribed available mailboxes
2803
This command is allowed in the Authenticated and Selected states.
2805
The parameters and returned object are the same as for the C{list}
2806
method, with one slight difference: Only mailboxes which have been
2807
subscribed can be included in the resulting list.
2810
args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
2812
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2813
d.addCallback(self.__cbList, 'LSUB')
2816
def __cbList(self, (lines, last), command):
2819
parts = parseNestedParens(L)
2821
raise IllegalServerResponse, L
2822
if parts[0] == command:
2823
parts[1] = tuple(parts[1])
2824
results.append(tuple(parts[1:]))
2827
def status(self, mailbox, *names):
2828
"""Retrieve the status of the given mailbox
2830
This command is allowed in the Authenticated and Selected states.
2832
@type mailbox: C{str}
2833
@param mailbox: The name of the mailbox to query
2836
@param names: The status names to query. These may be any number of:
2837
MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, and UNSEEN.
2840
@return: A deferred whose callback is invoked with the status information
2841
if the command is successful and whose errback is invoked otherwise.
2844
args = "%s (%s)" % (_prepareMailboxName(mailbox), ' '.join(names))
2846
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2847
d.addCallback(self.__cbStatus)
2850
def __cbStatus(self, (lines, last)):
2853
parts = parseNestedParens(line)
2854
if parts[0] == 'STATUS':
2856
items = [items[i:i+2] for i in range(0, len(items), 2)]
2857
status.update(dict(items))
2858
for k in status.keys():
2859
t = self.STATUS_TRANSFORMATIONS.get(k)
2862
status[k] = t(status[k])
2863
except Exception, e:
2864
raise IllegalServerResponse('(%s %s): %s' % (k, status[k], str(e)))
2867
def append(self, mailbox, message, flags = (), date = None):
2868
"""Add the given message to the given mailbox.
2870
This command is allowed in the Authenticated and Selected states.
2872
@type mailbox: C{str}
2873
@param mailbox: The mailbox to which to add this message.
2875
@type message: Any file-like object
2876
@param message: The message to add, in RFC822 format. Newlines
2877
in this file should be \\r\\n-style.
2879
@type flags: Any iterable of C{str}
2880
@param flags: The flags to associated with this message.
2883
@param date: The date to associate with this message. This should
2884
be of the format DD-MM-YYYY HH:MM:SS +/-HHMM. For example, in
2885
Eastern Standard Time, on July 1st 2004 at half past 1 PM,
2886
\"01-07-2004 13:30:00 -0500\".
2889
@return: A deferred whose callback is invoked when this command
2890
succeeds or whose errback is invoked if it fails.
2895
fmt = '%s (%s)%s {%d}'
2897
date = ' "%s"' % date
2901
_prepareMailboxName(mailbox), ' '.join(flags),
2904
d = self.sendCommand(Command('APPEND', cmd, (), self.__cbContinueAppend, message))
2907
def __cbContinueAppend(self, lines, message):
2908
s = basic.FileSender()
2909
return s.beginFileTransfer(message, self.transport, None
2910
).addCallback(self.__cbFinishAppend)
2912
def __cbFinishAppend(self, foo):
2916
"""Tell the server to perform a checkpoint
2918
This command is allowed in the Selected state.
2921
@return: A deferred whose callback is invoked when this command
2922
succeeds or whose errback is invoked if it fails.
2924
return self.sendCommand(Command('CHECK'))
2927
"""Return the connection to the Authenticated state.
2929
This command is allowed in the Selected state.
2931
Issuing this command will also remove all messages flagged \\Deleted
2932
from the selected mailbox if it is opened in read-write mode,
2933
otherwise it indicates success by no messages are removed.
2936
@return: A deferred whose callback is invoked when the command
2937
completes successfully or whose errback is invoked if it fails.
2939
return self.sendCommand(Command('CLOSE'))
2942
"""Return the connection to the Authenticate state.
2944
This command is allowed in the Selected state.
2946
Issuing this command will perform the same actions as issuing the
2947
close command, but will also generate an 'expunge' response for
2948
every message deleted.
2951
@return: A deferred whose callback is invoked with a list of the
2952
'expunge' responses when this command is successful or whose errback
2953
is invoked otherwise.
2957
d = self.sendCommand(Command(cmd, wantResponse=resp))
2958
d.addCallback(self.__cbExpunge)
2961
def __cbExpunge(self, (lines, last)):
2964
parts = line.split(None, 1)
2966
if parts[1] == 'EXPUNGE':
2968
ids.append(int(parts[0]))
2970
raise IllegalServerResponse, line
2973
def search(self, *queries, **kwarg):
2974
"""Search messages in the currently selected mailbox
2976
This command is allowed in the Selected state.
2978
Any non-zero number of queries are accepted by this method, as
2979
returned by the C{Query}, C{Or}, and C{Not} functions.
2981
One keyword argument is accepted: if uid is passed in with a non-zero
2982
value, the server is asked to return message UIDs instead of message
2986
@return: A deferred whose callback will be invoked with a list of all
2987
the message sequence numbers return by the search, or whose errback
2988
will be invoked if there is an error.
2990
if kwarg.get('uid'):
2994
args = ' '.join(queries)
2995
d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,)))
2996
d.addCallback(self.__cbSearch)
2999
def __cbSearch(self, (lines, end)):
3002
parts = line.split(None, 1)
3004
if parts[0] == 'SEARCH':
3006
ids.extend(map(int, parts[1].split()))
3008
raise IllegalServerResponse, line
3011
def fetchUID(self, messages, uid=0):
3012
"""Retrieve the unique identifier for one or more messages
3014
This command is allowed in the Selected state.
3016
@type messages: C{MessageSet} or C{str}
3017
@param messages: A message sequence set
3020
@param uid: Indicates whether the message sequence set is of message
3021
numbers or of unique message IDs.
3024
@return: A deferred whose callback is invoked with a dict mapping
3025
message sequence numbers to unique message identifiers, or whose
3026
errback is invoked if there is an error.
3028
d = self._fetch(messages, useUID=uid, uid=1)
3029
d.addCallback(self.__cbFetch)
3032
def fetchFlags(self, messages, uid=0):
3033
"""Retrieve the flags for one or more messages
3035
This command is allowed in the Selected state.
3037
@type messages: C{MessageSet} or C{str}
3038
@param messages: The messages for which to retrieve flags.
3041
@param uid: Indicates whether the message sequence set is of message
3042
numbers or of unique message IDs.
3045
@return: A deferred whose callback is invoked with a dict mapping
3046
message numbers to lists of flags, or whose errback is invoked if
3049
d = self._fetch(str(messages), useUID=uid, flags=1)
3050
d.addCallback(self.__cbFetch)
3053
def fetchInternalDate(self, messages, uid=0):
3054
"""Retrieve the internal date associated with one or more messages
3056
This command is allowed in the Selected state.
3058
@type messages: C{MessageSet} or C{str}
3059
@param messages: The messages for which to retrieve the internal date.
3062
@param uid: Indicates whether the message sequence set is of message
3063
numbers or of unique message IDs.
3066
@return: A deferred whose callback is invoked with a dict mapping
3067
message numbers to date strings, or whose errback is invoked
3068
if there is an error. Date strings take the format of
3069
\"day-month-year time timezone\".
3071
d = self._fetch(str(messages), useUID=uid, internaldate=1)
3072
d.addCallback(self.__cbFetch)
3075
def fetchEnvelope(self, messages, uid=0):
3076
"""Retrieve the envelope data for one or more messages
3078
This command is allowed in the Selected state.
3080
@type messages: C{MessageSet} or C{str}
3081
@param messages: The messages for which to retrieve envelope data.
3084
@param uid: Indicates whether the message sequence set is of message
3085
numbers or of unique message IDs.
3088
@return: A deferred whose callback is invoked with a dict mapping
3089
message numbers to envelope data, or whose errback is invoked
3090
if there is an error. Envelope data consists of a sequence of the
3091
date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
3092
and message-id header fields. The date, subject, in-reply-to, and
3093
message-id fields are strings, while the from, sender, reply-to,
3094
to, cc, and bcc fields contain address data. Address data consists
3095
of a sequence of name, source route, mailbox name, and hostname.
3096
Fields which are not present for a particular address may be C{None}.
3098
d = self._fetch(str(messages), useUID=uid, envelope=1)
3099
d.addCallback(self.__cbFetch)
3102
def fetchBodyStructure(self, messages, uid=0):
3103
"""Retrieve the structure of the body of one or more messages
3105
This command is allowed in the Selected state.
3107
@type messages: C{MessageSet} or C{str}
3108
@param messages: The messages for which to retrieve body structure
3112
@param uid: Indicates whether the message sequence set is of message
3113
numbers or of unique message IDs.
3116
@return: A deferred whose callback is invoked with a dict mapping
3117
message numbers to body structure data, or whose errback is invoked
3118
if there is an error. Body structure data describes the MIME-IMB
3119
format of a message and consists of a sequence of mime type, mime
3120
subtype, parameters, content id, description, encoding, and size.
3121
The fields following the size field are variable: if the mime
3122
type/subtype is message/rfc822, the contained message's envelope
3123
information, body structure data, and number of lines of text; if
3124
the mime type is text, the number of lines of text. Extension fields
3125
may also be included; if present, they are: the MD5 hash of the body,
3126
body disposition, body language.
3128
d = self._fetch(messages, useUID=uid, bodystructure=1)
3129
d.addCallback(self.__cbFetch)
3132
def fetchSimplifiedBody(self, messages, uid=0):
3133
"""Retrieve the simplified body structure of one or more messages
3135
This command is allowed in the Selected state.
3137
@type messages: C{MessageSet} or C{str}
3138
@param messages: A message sequence set
3141
@param uid: Indicates whether the message sequence set is of message
3142
numbers or of unique message IDs.
3145
@return: A deferred whose callback is invoked with a dict mapping
3146
message numbers to body data, or whose errback is invoked
3147
if there is an error. The simplified body structure is the same
3148
as the body structure, except that extension fields will never be
3151
d = self._fetch(messages, useUID=uid, body=1)
3152
d.addCallback(self.__cbFetch)
3155
def fetchMessage(self, messages, uid=0):
3156
"""Retrieve one or more entire messages
3158
This command is allowed in the Selected state.
3160
@type messages: C{MessageSet} or C{str}
3161
@param messages: A message sequence set
3164
@param uid: Indicates whether the message sequence set is of message
3165
numbers or of unique message IDs.
3168
@return: A deferred whose callback is invoked with a dict mapping
3169
message objects (as returned by self.messageFile(), file objects by
3170
default), to additional information, or whose errback is invoked if
3173
d = self._fetch(messages, useUID=uid, rfc822=1)
3174
d.addCallback(self.__cbFetch)
3177
def fetchHeaders(self, messages, uid=0):
3178
"""Retrieve headers of one or more messages
3180
This command is allowed in the Selected state.
3182
@type messages: C{MessageSet} or C{str}
3183
@param messages: A message sequence set
3186
@param uid: Indicates whether the message sequence set is of message
3187
numbers or of unique message IDs.
3190
@return: A deferred whose callback is invoked with a dict mapping
3191
message numbers to dicts of message headers, or whose errback is
3192
invoked if there is an error.
3194
d = self._fetch(messages, useUID=uid, rfc822header=1)
3195
d.addCallback(self.__cbFetch)
3198
def fetchBody(self, messages, uid=0):
3199
"""Retrieve body text of one or more messages
3201
This command is allowed in the Selected state.
3203
@type messages: C{MessageSet} or C{str}
3204
@param messages: A message sequence set
3207
@param uid: Indicates whether the message sequence set is of message
3208
numbers or of unique message IDs.
3211
@return: A deferred whose callback is invoked with a dict mapping
3212
message numbers to file-like objects containing body text, or whose
3213
errback is invoked if there is an error.
3215
d = self._fetch(messages, useUID=uid, rfc822text=1)
3216
d.addCallback(self.__cbFetch)
3219
def fetchSize(self, messages, uid=0):
3220
"""Retrieve the size, in octets, of one or more messages
3222
This command is allowed in the Selected state.
3224
@type messages: C{MessageSet} or C{str}
3225
@param messages: A message sequence set
3228
@param uid: Indicates whether the message sequence set is of message
3229
numbers or of unique message IDs.
3232
@return: A deferred whose callback is invoked with a dict mapping
3233
message numbers to sizes, or whose errback is invoked if there is
3236
d = self._fetch(messages, useUID=uid, rfc822size=1)
3237
d.addCallback(self.__cbFetch)
3240
def fetchFull(self, messages, uid=0):
3241
"""Retrieve several different fields of one or more messages
3243
This command is allowed in the Selected state. This is equivalent
3244
to issuing all of the C{fetchFlags}, C{fetchInternalDate},
3245
C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody}
3248
@type messages: C{MessageSet} or C{str}
3249
@param messages: A message sequence set
3252
@param uid: Indicates whether the message sequence set is of message
3253
numbers or of unique message IDs.
3256
@return: A deferred whose callback is invoked with a dict mapping
3257
message numbers to dict of the retrieved data values, or whose
3258
errback is invoked if there is an error. They dictionary keys
3259
are "flags", "date", "size", "envelope", and "body".
3262
messages, useUID=uid, flags=1, internaldate=1,
3263
rfc822size=1, envelope=1, body=1
3265
d.addCallback(self.__cbFetch)
3268
def fetchAll(self, messages, uid=0):
3269
"""Retrieve several different fields of one or more messages
3271
This command is allowed in the Selected state. This is equivalent
3272
to issuing all of the C{fetchFlags}, C{fetchInternalDate},
3273
C{fetchSize}, and C{fetchEnvelope} functions.
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 dict of the retrieved data values, or whose
3285
errback is invoked if there is an error. They dictionary keys
3286
are "flags", "date", "size", and "envelope".
3289
messages, useUID=uid, flags=1, internaldate=1,
3290
rfc822size=1, envelope=1
3292
d.addCallback(self.__cbFetch)
3295
def fetchFast(self, messages, uid=0):
3296
"""Retrieve several different fields of one or more messages
3298
This command is allowed in the Selected state. This is equivalent
3299
to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and
3300
C{fetchSize} functions.
3302
@type messages: C{MessageSet} or C{str}
3303
@param messages: A message sequence set
3306
@param uid: Indicates whether the message sequence set is of message
3307
numbers or of unique message IDs.
3310
@return: A deferred whose callback is invoked with a dict mapping
3311
message numbers to dict of the retrieved data values, or whose
3312
errback is invoked if there is an error. They dictionary keys are
3313
"flags", "date", and "size".
3316
messages, useUID=uid, flags=1, internaldate=1, rfc822size=1
3318
d.addCallback(self.__cbFetch)
3321
def __cbFetch(self, (lines, last)):
3324
parts = line.split(None, 2)
3326
if parts[1] == 'FETCH':
3330
raise IllegalServerResponse, line
3332
data = parseNestedParens(parts[2])
3333
while len(data) == 1 and isinstance(data, types.ListType):
3337
raise IllegalServerResponse("Not enough arguments", data)
3338
flags.setdefault(id, {})[data[0]] = data[1]
3341
print '(2)Ignoring ', parts
3343
print '(3)Ignoring ', parts
3346
def fetchSpecific(self, messages, uid=0, headerType=None,
3347
headerNumber=None, headerArgs=None, peek=None,
3348
offset=None, length=None):
3349
"""Retrieve a specific section of one or more messages
3351
@type messages: C{MessageSet} or C{str}
3352
@param messages: A message sequence set
3355
@param uid: Indicates whether the message sequence set is of message
3356
numbers or of unique message IDs.
3358
@type headerType: C{str}
3359
@param headerType: If specified, must be one of HEADER,
3360
HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, or TEXT, and will determine
3361
which part of the message is retrieved. For HEADER.FIELDS and
3362
HEADER.FIELDS.NOT, C{headerArgs} must be a sequence of header names.
3363
For MIME, C{headerNumber} must be specified.
3365
@type headerNumber: C{int} or C{int} sequence
3366
@param headerNumber: The nested rfc822 index specifying the
3367
entity to retrieve. For example, C{1} retrieves the first
3368
entity of the message, and C{(2, 1, 3}) retrieves the 3rd
3369
entity inside the first entity inside the second entity of
3372
@type headerArgs: A sequence of C{str}
3373
@param headerArgs: If C{headerType} is HEADER.FIELDS, these are the
3374
headers to retrieve. If it is HEADER.FIELDS.NOT, these are the
3375
headers to exclude from retrieval.
3378
@param peek: If true, cause the server to not set the \\Seen
3379
flag on this message as a result of this command.
3381
@type offset: C{int}
3382
@param offset: The number of octets at the beginning of the result
3385
@type length: C{int}
3386
@param length: The number of octets to retrieve.
3389
@return: A deferred whose callback is invoked with a mapping of
3390
message numbers to retrieved data, or whose errback is invoked
3391
if there is an error.
3393
fmt = '%s BODY%s[%s%s%s]%s'
3394
if headerNumber is None:
3396
elif isinstance(headerNumber, types.IntType):
3397
number = str(headerNumber)
3399
number = '.'.join(headerNumber)
3400
if headerType is None:
3403
header = '.' + headerType
3407
if headerArgs is not None:
3408
payload = ' (%s)' % ' '.join(headerArgs)
3416
extra = '<%d.%d>' % (offset, length)
3417
fetch = uid and 'UID FETCH' or 'FETCH'
3418
cmd = fmt % (messages, peek and '.PEEK' or '', number, header, payload, extra)
3419
d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
3420
d.addCallback(self.__cbFetchSpecific)
3423
def __cbFetchSpecific(self, (lines, last)):
3426
parts = line.split(None, 2)
3428
if parts[1] == 'FETCH':
3432
raise IllegalServerResponse, line
3434
info[id] = parseNestedParens(parts[2])
3437
def _fetch(self, messages, useUID=0, **terms):
3438
fetch = useUID and 'UID FETCH' or 'FETCH'
3440
if 'rfc822text' in terms:
3441
del terms['rfc822text']
3442
terms['rfc822.text'] = True
3443
if 'rfc822size' in terms:
3444
del terms['rfc822size']
3445
terms['rfc822.size'] = True
3446
if 'rfc822header' in terms:
3447
del terms['rfc822header']
3448
terms['rfc822.header'] = True
3450
cmd = '%s (%s)' % (messages, ' '.join([s.upper() for s in terms.keys()]))
3451
d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
3454
def setFlags(self, messages, flags, silent=1, uid=0):
3455
"""Set the flags for one or more messages.
3457
This command is allowed in the Selected state.
3459
@type messages: C{MessageSet} or C{str}
3460
@param messages: A message sequence set
3462
@type flags: Any iterable of C{str}
3463
@param flags: The flags to set
3465
@type silent: C{bool}
3466
@param silent: If true, cause the server to supress its verbose
3470
@param uid: Indicates whether the message sequence set is of message
3471
numbers or of unique message IDs.
3474
@return: A deferred whose callback is invoked with a list of the
3475
the server's responses (C{[]} if C{silent} is true) or whose
3476
errback is invoked if there is an error.
3478
return self._store(str(messages), silent and 'FLAGS.SILENT' or 'FLAGS', flags, uid)
3480
def addFlags(self, messages, flags, silent=1, uid=0):
3481
"""Add to the set flags for one or more messages.
3483
This command is allowed in the Selected state.
3485
@type messages: C{MessageSet} or C{str}
3486
@param messages: A message sequence set
3488
@type flags: Any iterable of C{str}
3489
@param flags: The flags to set
3491
@type silent: C{bool}
3492
@param silent: If true, cause the server to supress its verbose
3496
@param uid: Indicates whether the message sequence set is of message
3497
numbers or of unique message IDs.
3500
@return: A deferred whose callback is invoked with a list of the
3501
the server's responses (C{[]} if C{silent} is true) or whose
3502
errback is invoked if there is an error.
3504
return self._store(str(messages), silent and '+FLAGS.SILENT' or '+FLAGS', flags, uid)
3506
def removeFlags(self, messages, flags, silent=1, uid=0):
3507
"""Remove from the set flags for one or more messages.
3509
This command is allowed in the Selected state.
3511
@type messages: C{MessageSet} or C{str}
3512
@param messages: A message sequence set
3514
@type flags: Any iterable of C{str}
3515
@param flags: The flags to set
3517
@type silent: C{bool}
3518
@param silent: If true, cause the server to supress its verbose
3522
@param uid: Indicates whether the message sequence set is of message
3523
numbers or of unique message IDs.
3526
@return: A deferred whose callback is invoked with a list of the
3527
the server's responses (C{[]} if C{silent} is true) or whose
3528
errback is invoked if there is an error.
3530
return self._store(str(messages), silent and '-FLAGS.SILENT' or '-FLAGS', flags, uid)
3532
def _store(self, messages, cmd, flags, uid):
3533
store = uid and 'UID STORE' or 'STORE'
3534
args = ' '.join((messages, cmd, '(%s)' % ' '.join(flags)))
3535
d = self.sendCommand(Command(store, args, wantResponse=('FETCH',)))
3536
d.addCallback(self.__cbFetch)
3539
def copy(self, messages, mailbox, uid):
3540
"""Copy the specified messages to the specified mailbox.
3542
This command is allowed in the Selected state.
3544
@type messages: C{str}
3545
@param messages: A message sequence set
3547
@type mailbox: C{str}
3548
@param mailbox: The mailbox to which to copy the messages
3551
@param uid: If true, the C{messages} refers to message UIDs, rather
3552
than message sequence numbers.
3555
@return: A deferred whose callback is invoked with a true value
3556
when the copy is successful, or whose errback is invoked if there
3563
args = '%s %s' % (messages, _prepareMailboxName(mailbox))
3564
return self.sendCommand(Command(cmd, args))
3567
# IMailboxListener methods
3569
def modeChanged(self, writeable):
3572
def flagsChanged(self, newFlags):
3575
def newMessages(self, exists, recent):
3579
class IllegalIdentifierError(IMAP4Exception): pass
3583
parts = s.split(',')
3586
low, high = p.split(':', 1)
3596
res.extend((low, high))
3598
raise IllegalIdentifierError(p)
3606
raise IllegalIdentifierError(p)
3611
class IllegalQueryError(IMAP4Exception): pass
3614
'ALL', 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'NEW', 'OLD', 'RECENT',
3615
'SEEN', 'UNANSWERED', 'UNDELETED', 'UNDRAFT', 'UNFLAGGED', 'UNSEEN'
3619
'LARGER', 'SMALLER', 'UID'
3622
def Query(sorted=0, **kwarg):
3623
"""Create a query string
3625
Among the accepted keywords are::
3627
all : If set to a true value, search all messages in the
3630
answered : If set to a true value, search messages flagged with
3633
bcc : A substring to search the BCC header field for
3635
before : Search messages with an internal date before this
3636
value. The given date should be a string in the format
3637
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3639
body : A substring to search the body of the messages for
3641
cc : A substring to search the CC header field for
3643
deleted : If set to a true value, search messages flagged with
3646
draft : If set to a true value, search messages flagged with
3649
flagged : If set to a true value, search messages flagged with
3652
from : A substring to search the From header field for
3654
header : A two-tuple of a header name and substring to search
3657
keyword : Search for messages with the given keyword set
3659
larger : Search for messages larger than this number of octets
3661
messages : Search only the given message sequence set.
3663
new : If set to a true value, search messages flagged with
3664
\\Recent but not \\Seen
3666
old : If set to a true value, search messages not flagged with
3669
on : Search messages with an internal date which is on this
3670
date. The given date should be a string in the format
3671
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3673
recent : If set to a true value, search for messages flagged with
3676
seen : If set to a true value, search for messages flagged with
3679
sentbefore : Search for messages with an RFC822 'Date' header before
3680
this date. The given date should be a string in the format
3681
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3683
senton : Search for messages with an RFC822 'Date' header which is
3684
on this date The given date should be a string in the format
3685
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3687
sentsince : Search for messages with an RFC822 'Date' header which is
3688
after this date. The given date should be a string in the format
3689
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3691
since : Search for messages with an internal date that is after
3692
this date.. The given date should be a string in the format
3693
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3695
smaller : Search for messages smaller than this number of octets
3697
subject : A substring to search the 'subject' header for
3699
text : A substring to search the entire message for
3701
to : A substring to search the 'to' header for
3703
uid : Search only the messages in the given message set
3705
unanswered : If set to a true value, search for messages not
3706
flagged with \\Answered
3708
undeleted : If set to a true value, search for messages not
3709
flagged with \\Deleted
3711
undraft : If set to a true value, search for messages not
3712
flagged with \\Draft
3714
unflagged : If set to a true value, search for messages not
3715
flagged with \\Flagged
3717
unkeyword : Search for messages without the given keyword set
3719
unseen : If set to a true value, search for messages not
3722
@type sorted: C{bool}
3723
@param sorted: If true, the output will be sorted, alphabetically.
3724
The standard does not require it, but it makes testing this function
3725
easier. The default is zero, and this should be acceptable for any
3729
@return: The formatted query string
3738
if k in _SIMPLE_BOOL and v:
3741
cmd.extend([k, v[0], '"%s"' % (v[1],)])
3742
elif k not in _NO_QUOTES:
3743
cmd.extend([k, '"%s"' % (v,)])
3745
cmd.extend([k, '%s' % (v,)])
3747
return '(%s)' % ' '.join(cmd)
3749
return ' '.join(cmd)
3752
"""The disjunction of two or more queries"""
3754
raise IllegalQueryError, args
3755
elif len(args) == 2:
3756
return '(OR %s %s)' % args
3758
return '(OR %s %s)' % (args[0], Or(*args[1:]))
3761
"""The negation of a query"""
3762
return '(NOT %s)' % (query,)
3764
class MismatchedNesting(IMAP4Exception):
3767
class MismatchedQuoting(IMAP4Exception):
3770
def wildcardToRegexp(wildcard, delim=None):
3771
wildcard = wildcard.replace('*', '(?:.*?)')
3773
wildcard = wildcard.replace('%', '(?:.*?)')
3775
wildcard = wildcard.replace('%', '(?:(?:[^%s])*?)' % re.escape(delim))
3776
return re.compile(wildcard, re.I)
3779
"""Split a string into whitespace delimited tokens
3781
Tokens that would otherwise be separated but are surrounded by \"
3782
remain as a single token. Any token that is not quoted and is
3783
equal to \"NIL\" is tokenized as C{None}.
3786
@param s: The string to be split
3788
@rtype: C{list} of C{str}
3789
@return: A list of the resulting tokens
3791
@raise MismatchedQuoting: Raised if an odd number of quotes are present
3795
inQuote = inWord = start = 0
3796
for (i, c) in zip(range(len(s)), s):
3797
if c == '"' and not inQuote:
3800
elif c == '"' and inQuote:
3802
result.append(s[start:i])
3804
elif not inWord and not inQuote and c not in ('"' + string.whitespace):
3807
elif inWord and not inQuote and c in string.whitespace:
3808
if s[start:i] == 'NIL':
3811
result.append(s[start:i])
3815
raise MismatchedQuoting(s)
3817
if s[start:] == 'NIL':
3820
result.append(s[start:])
3824
def splitOn(sequence, predicate, transformers):
3826
mode = predicate(sequence[0])
3828
for e in sequence[1:]:
3831
result.extend(transformers[mode](tmp))
3836
result.extend(transformers[mode](tmp))
3839
def collapseStrings(results):
3841
Turns a list of length-one strings and lists into a list of longer
3842
strings and lists. For example,
3844
['a', 'b', ['c', 'd']] is returned as ['ab', ['cd']]
3846
@type results: C{list} of C{str} and C{list}
3847
@param results: The list to be collapsed
3849
@rtype: C{list} of C{str} and C{list}
3850
@return: A new list which is the collapsed form of C{results}
3854
listsList = [isinstance(s, types.ListType) for s in results]
3856
pred = lambda e: isinstance(e, types.TupleType)
3858
0: lambda e: splitQuoted(''.join(e)),
3859
1: lambda e: [''.join([i[0] for i in e])]
3861
for (i, c, isList) in zip(range(len(results)), results, listsList):
3863
if begun is not None:
3864
copy.extend(splitOn(results[begun:i], pred, tran))
3866
copy.append(collapseStrings(c))
3869
if begun is not None:
3870
copy.extend(splitOn(results[begun:], pred, tran))
3874
def parseNestedParens(s, handleLiteral = 1):
3875
"""Parse an s-exp-like string into a more useful data structure.
3878
@param s: The s-exp-like string to parse
3880
@rtype: C{list} of C{str} and C{list}
3881
@return: A list containing the tokens present in the input.
3883
@raise MismatchedNesting: Raised if the number or placement
3884
of opening or closing parenthesis is invalid.
3896
contentStack[-1].append(s[i+1])
3900
inQuote = not inQuote
3901
contentStack[-1].append(c)
3905
contentStack[-1].append(c)
3906
inQuote = not inQuote
3908
elif handleLiteral and c == '{':
3909
end = s.find('}', i)
3911
raise ValueError, "Malformed literal"
3912
literalSize = int(s[i+1:end])
3913
contentStack[-1].append((s[end+3:end+3+literalSize],))
3914
i = end + 3 + literalSize
3915
elif c == '(' or c == '[':
3916
contentStack.append([])
3918
elif c == ')' or c == ']':
3919
contentStack[-2].append(contentStack.pop())
3922
contentStack[-1].append(c)
3925
raise MismatchedNesting(s)
3926
if len(contentStack) != 1:
3927
raise MismatchedNesting(s)
3928
return collapseStrings(contentStack[0])
3931
return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),)
3934
return '{%d}\r\n%s' % (len(s), s)
3937
def __init__(self, value):
3941
return str(self.value)
3943
_ATOM_SPECIALS = '(){ %*"'
3948
if c < '\x20' or c > '\x7f':
3950
if c in _ATOM_SPECIALS:
3954
def _prepareMailboxName(name):
3955
name = name.encode('imap4-utf-7')
3956
if _needsQuote(name):
3960
def _needsLiteral(s):
3961
# Change this to "return 1" to wig out stupid clients
3962
return '\n' in s or '\r' in s or len(s) > 1000
3964
def collapseNestedLists(items):
3965
"""Turn a nested list structure into an s-exp-like string.
3967
Strings in C{items} will be sent as literals if they contain CR or LF,
3968
otherwise they will be quoted. References to None in C{items} will be
3969
translated to the atom NIL. Objects with a 'read' attribute will have
3970
it called on them with no arguments and the returned string will be
3971
inserted into the output as a literal. Integers will be converted to
3972
strings and inserted into the output unquoted. Instances of
3973
C{DontQuoteMe} will be converted to strings and inserted into the output
3976
This function used to be much nicer, and only quote things that really
3977
needed to be quoted (and C{DontQuoteMe} did not exist), however, many
3978
broken IMAP4 clients were unable to deal with this level of sophistication,
3979
forcing the current behavior to be adopted for practical reasons.
3981
@type items: Any iterable
3988
pieces.extend([' ', 'NIL'])
3989
elif isinstance(i, (DontQuoteMe, int, long)):
3990
pieces.extend([' ', str(i)])
3991
elif isinstance(i, types.StringTypes):
3992
if _needsLiteral(i):
3993
pieces.extend([' ', '{', str(len(i)), '}', IMAP4Server.delimiter, i])
3995
pieces.extend([' ', _quote(i)])
3996
elif hasattr(i, 'read'):
3998
pieces.extend([' ', '{', str(len(d)), '}', IMAP4Server.delimiter, d])
4000
pieces.extend([' ', '(%s)' % (collapseNestedLists(i),)])
4001
return ''.join(pieces[1:])
4004
class IClientAuthentication(Interface):
4006
"""Return an identifier associated with this authentication scheme.
4011
def challengeResponse(secret, challenge):
4012
"""Generate a challenge response string"""
4014
class CramMD5ClientAuthenticator:
4015
implements(IClientAuthentication)
4017
def __init__(self, user):
4023
def challengeResponse(self, secret, chal):
4024
response = hmac.HMAC(secret, chal).hexdigest()
4025
return '%s %s' % (self.user, response)
4027
class LOGINAuthenticator:
4028
implements(IClientAuthentication)
4030
def __init__(self, user):
4032
self.challengeResponse = self.challengeUsername
4037
def challengeUsername(self, secret, chal):
4038
# Respond to something like "Username:"
4039
self.challengeResponse = self.challengeSecret
4042
def challengeSecret(self, secret, chal):
4043
# Respond to something like "Password:"
4046
class PLAINAuthenticator:
4047
implements(IClientAuthentication)
4049
def __init__(self, user):
4055
def challengeResponse(self, secret, chal):
4056
return '%s\0%s\0' % (self.user, secret)
4059
class MailboxException(IMAP4Exception): pass
4061
class MailboxCollision(MailboxException):
4063
return 'Mailbox named %s already exists' % self.args
4065
class NoSuchMailbox(MailboxException):
4067
return 'No mailbox named %s exists' % self.args
4069
class ReadOnlyMailbox(MailboxException):
4071
return 'Mailbox open in read-only state'
4074
class IAccount(Interface):
4075
"""Interface for Account classes
4077
Implementors of this interface should consider implementing
4078
C{INamespacePresenter}.
4081
def addMailbox(name, mbox = None):
4082
"""Add a new mailbox to this account
4085
@param name: The name associated with this mailbox. It may not
4086
contain multiple hierarchical parts.
4088
@type mbox: An object implementing C{IMailbox}
4089
@param mbox: The mailbox to associate with this name. If C{None},
4090
a suitable default is created and used.
4092
@rtype: C{Deferred} or C{bool}
4093
@return: A true value if the creation succeeds, or a deferred whose
4094
callback will be invoked when the creation succeeds.
4096
@raise MailboxException: Raised if this mailbox cannot be added for
4097
some reason. This may also be raised asynchronously, if a C{Deferred}
4101
def create(pathspec):
4102
"""Create a new mailbox from the given hierarchical name.
4104
@type pathspec: C{str}
4105
@param pathspec: The full hierarchical name of a new mailbox to create.
4106
If any of the inferior hierarchical names to this one do not exist,
4107
they are created as well.
4109
@rtype: C{Deferred} or C{bool}
4110
@return: A true value if the creation succeeds, or a deferred whose
4111
callback will be invoked when the creation succeeds.
4113
@raise MailboxException: Raised if this mailbox cannot be added.
4114
This may also be raised asynchronously, if a C{Deferred} is
4118
def select(name, rw=True):
4119
"""Acquire a mailbox, given its name.
4122
@param name: The mailbox to acquire
4125
@param rw: If a true value, request a read-write version of this
4126
mailbox. If a false value, request a read-only version.
4128
@rtype: Any object implementing C{IMailbox} or C{Deferred}
4129
@return: The mailbox object, or a C{Deferred} whose callback will
4130
be invoked with the mailbox object. None may be returned if the
4131
specified mailbox may not be selected for any reason.
4135
"""Delete the mailbox with the specified name.
4138
@param name: The mailbox to delete.
4140
@rtype: C{Deferred} or C{bool}
4141
@return: A true value if the mailbox is successfully deleted, or a
4142
C{Deferred} whose callback will be invoked when the deletion
4145
@raise MailboxException: Raised if this mailbox cannot be deleted.
4146
This may also be raised asynchronously, if a C{Deferred} is returned.
4149
def rename(oldname, newname):
4152
@type oldname: C{str}
4153
@param oldname: The current name of the mailbox to rename.
4155
@type newname: C{str}
4156
@param newname: The new name to associate with the mailbox.
4158
@rtype: C{Deferred} or C{bool}
4159
@return: A true value if the mailbox is successfully renamed, or a
4160
C{Deferred} whose callback will be invoked when the rename operation
4163
@raise MailboxException: Raised if this mailbox cannot be
4164
renamed. This may also be raised asynchronously, if a C{Deferred}
4168
def isSubscribed(name):
4169
"""Check the subscription status of a mailbox
4172
@param name: The name of the mailbox to check
4174
@rtype: C{Deferred} or C{bool}
4175
@return: A true value if the given mailbox is currently subscribed
4176
to, a false value otherwise. A C{Deferred} may also be returned
4177
whose callback will be invoked with one of these values.
4180
def subscribe(name):
4181
"""Subscribe to a mailbox
4184
@param name: The name of the mailbox to subscribe to
4186
@rtype: C{Deferred} or C{bool}
4187
@return: A true value if the mailbox is subscribed to successfully,
4188
or a Deferred whose callback will be invoked with this value when
4189
the subscription is successful.
4191
@raise MailboxException: Raised if this mailbox cannot be
4192
subscribed to. This may also be raised asynchronously, if a
4193
C{Deferred} is returned.
4196
def unsubscribe(name):
4197
"""Unsubscribe from a mailbox
4200
@param name: The name of the mailbox to unsubscribe from
4202
@rtype: C{Deferred} or C{bool}
4203
@return: A true value if the mailbox is unsubscribed from successfully,
4204
or a Deferred whose callback will be invoked with this value when
4205
the unsubscription is successful.
4207
@raise MailboxException: Raised if this mailbox cannot be
4208
unsubscribed from. This may also be raised asynchronously, if a
4209
C{Deferred} is returned.
4212
def listMailboxes(ref, wildcard):
4213
"""List all the mailboxes that meet a certain criteria
4216
@param ref: The context in which to apply the wildcard
4218
@type wildcard: C{str}
4219
@param wildcard: An expression against which to match mailbox names.
4220
'*' matches any number of characters in a mailbox name, and '%'
4221
matches similarly, but will not match across hierarchical boundaries.
4223
@rtype: C{list} of C{tuple}
4224
@return: A list of C{(mailboxName, mailboxObject)} which meet the
4225
given criteria. C{mailboxObject} should implement either
4226
C{IMailboxInfo} or C{IMailbox}. A Deferred may also be returned.
4229
class INamespacePresenter(Interface):
4230
def getPersonalNamespaces():
4231
"""Report the available personal namespaces.
4233
Typically there should be only one personal namespace. A common
4234
name for it is \"\", and its hierarchical delimiter is usually
4237
@rtype: iterable of two-tuples of strings
4238
@return: The personal namespaces and their hierarchical delimiters.
4239
If no namespaces of this type exist, None should be returned.
4242
def getSharedNamespaces():
4243
"""Report the available shared namespaces.
4245
Shared namespaces do not belong to any individual user but are
4246
usually to one or more of them. Examples of shared namespaces
4247
might be \"#news\" for a usenet gateway.
4249
@rtype: iterable of two-tuples of strings
4250
@return: The shared namespaces and their hierarchical delimiters.
4251
If no namespaces of this type exist, None should be returned.
4254
def getUserNamespaces():
4255
"""Report the available user namespaces.
4257
These are namespaces that contain folders belonging to other users
4258
access to which this account has been granted.
4260
@rtype: iterable of two-tuples of strings
4261
@return: The user namespaces and their hierarchical delimiters.
4262
If no namespaces of this type exist, None should be returned.
4266
class MemoryAccount(object):
4267
implements(IAccount, INamespacePresenter)
4270
subscriptions = None
4273
def __init__(self, name):
4276
self.subscriptions = []
4278
def allocateID(self):
4286
def addMailbox(self, name, mbox = None):
4288
if self.mailboxes.has_key(name):
4289
raise MailboxCollision, name
4291
mbox = self._emptyMailbox(name, self.allocateID())
4292
self.mailboxes[name] = mbox
4295
def create(self, pathspec):
4296
paths = filter(None, pathspec.split('/'))
4297
for accum in range(1, len(paths)):
4299
self.addMailbox('/'.join(paths[:accum]))
4300
except MailboxCollision:
4303
self.addMailbox('/'.join(paths))
4304
except MailboxCollision:
4305
if not pathspec.endswith('/'):
4309
def _emptyMailbox(self, name, id):
4310
raise NotImplementedError
4312
def select(self, name, readwrite=1):
4313
return self.mailboxes.get(name.upper())
4315
def delete(self, name):
4317
# See if this mailbox exists at all
4318
mbox = self.mailboxes.get(name)
4320
raise MailboxException("No such mailbox")
4321
# See if this box is flagged \Noselect
4322
if r'\Noselect' in mbox.getFlags():
4323
# Check for hierarchically inferior mailboxes with this one
4324
# as part of their root.
4325
for others in self.mailboxes.keys():
4326
if others != name and others.startswith(name):
4327
raise MailboxException, "Hierarchically inferior mailboxes exist and \\Noselect is set"
4330
# iff there are no hierarchically inferior names, we will
4331
# delete it from our ken.
4332
if self._inferiorNames(name) > 1:
4333
del self.mailboxes[name]
4335
def rename(self, oldname, newname):
4336
oldname = oldname.upper()
4337
newname = newname.upper()
4338
if not self.mailboxes.has_key(oldname):
4339
raise NoSuchMailbox, oldname
4341
inferiors = self._inferiorNames(oldname)
4342
inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
4344
for (old, new) in inferiors:
4345
if self.mailboxes.has_key(new):
4346
raise MailboxCollision, new
4348
for (old, new) in inferiors:
4349
self.mailboxes[new] = self.mailboxes[old]
4350
del self.mailboxes[old]
4352
def _inferiorNames(self, name):
4354
for infname in self.mailboxes.keys():
4355
if infname.startswith(name):
4356
inferiors.append(infname)
4359
def isSubscribed(self, name):
4360
return name.upper() in self.subscriptions
4362
def subscribe(self, name):
4364
if name not in self.subscriptions:
4365
self.subscriptions.append(name)
4367
def unsubscribe(self, name):
4369
if name not in self.subscriptions:
4370
raise MailboxException, "Not currently subscribed to " + name
4371
self.subscriptions.remove(name)
4373
def listMailboxes(self, ref, wildcard):
4374
ref = self._inferiorNames(ref.upper())
4375
wildcard = wildcardToRegexp(wildcard, '/')
4376
return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]
4379
## INamespacePresenter
4381
def getPersonalNamespaces(self):
4384
def getSharedNamespaces(self):
4387
def getOtherNamespaces(self):
4392
_statusRequestDict = {
4393
'MESSAGES': 'getMessageCount',
4394
'RECENT': 'getRecentCount',
4395
'UIDNEXT': 'getUIDNext',
4396
'UIDVALIDITY': 'getUIDValidity',
4397
'UNSEEN': 'getUnseenCount'
4399
def statusRequestHelper(mbox, names):
4402
r[n] = getattr(mbox, _statusRequestDict[n.upper()])()
4405
def parseAddr(addr):
4407
return [(None, None, None),]
4408
addrs = email.Utils.getaddresses([addr])
4409
return [[fn or None, None] + addr.split('@') for fn, addr in addrs]
4411
def getEnvelope(msg):
4412
headers = msg.getHeaders(True)
4413
date = headers.get('date')
4414
subject = headers.get('subject')
4415
from_ = headers.get('from')
4416
sender = headers.get('sender', from_)
4417
reply_to = headers.get('reply-to', from_)
4418
to = headers.get('to')
4419
cc = headers.get('cc')
4420
bcc = headers.get('bcc')
4421
in_reply_to = headers.get('in-reply-to')
4422
mid = headers.get('message-id')
4423
return (date, subject, parseAddr(from_), parseAddr(sender),
4424
reply_to and parseAddr(reply_to), to and parseAddr(to),
4425
cc and parseAddr(cc), bcc and parseAddr(bcc), in_reply_to, mid)
4427
def getLineCount(msg):
4428
# XXX - Super expensive, CACHE THIS VALUE FOR LATER RE-USE
4429
# XXX - This must be the number of lines in the ENCODED version
4431
for _ in msg.getBodyFile():
4436
if s[0] == s[-1] == '"':
4440
def getBodyStructure(msg, extended=False):
4441
# XXX - This does not properly handle multipart messages
4442
# BODYSTRUCTURE is obscenely complex and criminally under-documented.
4445
headers = 'content-type', 'content-id', 'content-description', 'content-transfer-encoding'
4446
headers = msg.getHeaders(False, *headers)
4447
mm = headers.get('content-type')
4449
mm = ''.join(mm.splitlines())
4450
mimetype = mm.split(';')
4452
type = mimetype[0].split('/', 1)
4456
elif len(type) == 2:
4459
major = minor = None
4460
attrs = dict([x.strip().lower().split('=', 1) for x in mimetype[1:]])
4462
major = minor = None
4464
major = minor = None
4467
size = str(msg.getSize())
4468
unquotedAttrs = [(k, unquote(v)) for (k, v) in attrs.iteritems()]
4470
major, minor, # Main and Sub MIME types
4471
unquotedAttrs, # content-type parameter list
4472
headers.get('content-id'),
4473
headers.get('content-description'),
4474
headers.get('content-transfer-encoding'),
4475
size, # Number of octets total
4478
if major is not None:
4479
if major.lower() == 'text':
4480
result.append(str(getLineCount(msg)))
4481
elif (major.lower(), minor.lower()) == ('message', 'rfc822'):
4482
contained = msg.getSubPart(0)
4483
result.append(getEnvelope(contained))
4484
result.append(getBodyStructure(contained, False))
4485
result.append(str(getLineCount(contained)))
4487
if not extended or major is None:
4490
if major.lower() != 'multipart':
4491
headers = 'content-md5', 'content-disposition', 'content-language'
4492
headers = msg.getHeaders(False, *headers)
4493
disp = headers.get('content-disposition')
4495
# XXX - I dunno if this is really right
4497
disp = disp.split('; ')
4499
disp = (disp[0].lower(), None)
4501
disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
4503
result.append(headers.get('content-md5'))
4505
result.append(headers.get('content-language'))
4511
submsg = msg.getSubPart(i)
4512
result.append(getBodyStructure(submsg))
4515
result.append(minor)
4516
result.append(attrs.items())
4518
# XXX - I dunno if this is really right
4519
headers = msg.getHeaders(False, 'content-disposition', 'content-language')
4520
disp = headers.get('content-disposition')
4522
disp = disp.split('; ')
4524
disp = (disp[0].lower(), None)
4526
disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
4529
result.append(headers.get('content-language'))
4533
class IMessagePart(Interface):
4534
def getHeaders(negate, *names):
4535
"""Retrieve a group of message headers.
4537
@type names: C{tuple} of C{str}
4538
@param names: The names of the headers to retrieve or omit.
4540
@type negate: C{bool}
4541
@param negate: If True, indicates that the headers listed in C{names}
4542
should be omitted from the return value, rather than included.
4545
@return: A mapping of header field names to header field values
4549
"""Retrieve a file object containing only the body of this message.
4553
"""Retrieve the total size, in octets, of this message.
4559
"""Indicate whether this message has subparts.
4564
def getSubPart(part):
4565
"""Retrieve a MIME sub-message
4568
@param part: The number of the part to retrieve, indexed from 0.
4570
@raise IndexError: Raised if the specified part does not exist.
4571
@raise TypeError: Raised if this message is not multipart.
4573
@rtype: Any object implementing C{IMessagePart}.
4574
@return: The specified sub-part.
4577
class IMessage(IMessagePart):
4579
"""Retrieve the unique identifier associated with this message.
4583
"""Retrieve the flags associated with this message.
4586
@return: The flags, represented as strings.
4589
def getInternalDate():
4590
"""Retrieve the date internally associated with this message.
4593
@return: An RFC822-formatted date string.
4596
class IMessageFile(Interface):
4597
"""Optional message interface for representing messages as files.
4599
If provided by message objects, this interface will be used instead
4600
the more complex MIME-based interface.
4603
"""Return an file-like object opened for reading.
4605
Reading from the returned file will return all the bytes
4606
of which this message consists.
4609
class ISearchableMailbox(Interface):
4610
def search(query, uid):
4611
"""Search for messages that meet the given query criteria.
4613
If this interface is not implemented by the mailbox, L{IMailbox.fetch}
4614
and various methods of L{IMessage} will be used instead.
4616
Implementations which wish to offer better performance than the
4617
default implementation should implement this interface.
4619
@type query: C{list}
4620
@param query: The search criteria
4623
@param uid: If true, the IDs specified in the query are UIDs;
4624
otherwise they are message sequence IDs.
4626
@rtype: C{list} or C{Deferred}
4627
@return: A list of message sequence numbers or message UIDs which
4628
match the search criteria or a C{Deferred} whose callback will be
4629
invoked with such a list.
4632
class IMessageCopier(Interface):
4633
def copy(messageObject):
4634
"""Copy the given message object into this mailbox.
4636
The message object will be one which was previously returned by
4639
Implementations which wish to offer better performance than the
4640
default implementation should implement this interface.
4642
If this interface is not implemented by the mailbox, IMailbox.addMessage
4643
will be used instead.
4645
@rtype: C{Deferred} or C{int}
4646
@return: Either the UID of the message or a Deferred which fires
4647
with the UID when the copy finishes.
4650
class IMailboxInfo(Interface):
4651
"""Interface specifying only the methods required for C{listMailboxes}.
4653
Implementations can return objects implementing only these methods for
4654
return to C{listMailboxes} if it can allow them to operate more
4659
"""Return the flags defined in this mailbox
4661
Flags with the \\ prefix are reserved for use as system flags.
4663
@rtype: C{list} of C{str}
4664
@return: A list of the flags that can be set on messages in this mailbox.
4667
def getHierarchicalDelimiter():
4668
"""Get the character which delimits namespaces for in this mailbox.
4673
class IMailbox(IMailboxInfo):
4674
def getUIDValidity():
4675
"""Return the unique validity identifier for this mailbox.
4681
"""Return the likely UID for the next message added to this mailbox.
4686
def getUID(message):
4687
"""Return the UID of a message in the mailbox
4689
@type message: C{int}
4690
@param message: The message sequence number
4693
@return: The UID of the message.
4696
def getMessageCount():
4697
"""Return the number of messages in this mailbox.
4702
def getRecentCount():
4703
"""Return the number of messages with the 'Recent' flag.
4708
def getUnseenCount():
4709
"""Return the number of messages with the 'Unseen' flag.
4715
"""Get the read/write status of the mailbox.
4718
@return: A true value if write permission is allowed, a false value otherwise.
4722
"""Called before this mailbox is deleted, permanently.
4724
If necessary, all resources held by this mailbox should be cleaned
4725
up here. This function _must_ set the \\Noselect flag on this
4729
def requestStatus(names):
4730
"""Return status information about this mailbox.
4732
Mailboxes which do not intend to do any special processing to
4733
generate the return value, C{statusRequestHelper} can be used
4734
to build the dictionary by calling the other interface methods
4735
which return the data for each name.
4737
@type names: Any iterable
4738
@param names: The status names to return information regarding.
4739
The possible values for each name are: MESSAGES, RECENT, UIDNEXT,
4740
UIDVALIDITY, UNSEEN.
4742
@rtype: C{dict} or C{Deferred}
4743
@return: A dictionary containing status information about the
4744
requested names is returned. If the process of looking this
4745
information up would be costly, a deferred whose callback will
4746
eventually be passed this dictionary is returned instead.
4749
def addListener(listener):
4750
"""Add a mailbox change listener
4752
@type listener: Any object which implements C{IMailboxListener}
4753
@param listener: An object to add to the set of those which will
4754
be notified when the contents of this mailbox change.
4757
def removeListener(listener):
4758
"""Remove a mailbox change listener
4760
@type listener: Any object previously added to and not removed from
4761
this mailbox as a listener.
4762
@param listener: The object to remove from the set of listeners.
4764
@raise ValueError: Raised when the given object is not a listener for
4768
def addMessage(message, flags = (), date = None):
4769
"""Add the given message to this mailbox.
4771
@type message: A file-like object
4772
@param message: The RFC822 formatted message
4774
@type flags: Any iterable of C{str}
4775
@param flags: The flags to associate with this message
4778
@param date: If specified, the date to associate with this
4782
@return: A deferred whose callback is invoked with the message
4783
id if the message is added successfully and whose errback is
4786
@raise ReadOnlyMailbox: Raised if this Mailbox is not open for
4791
"""Remove all messages flagged \\Deleted.
4793
@rtype: C{list} or C{Deferred}
4794
@return: The list of message sequence numbers which were deleted,
4795
or a C{Deferred} whose callback will be invoked with such a list.
4797
@raise ReadOnlyMailbox: Raised if this Mailbox is not open for
4801
def fetch(messages, uid):
4802
"""Retrieve one or more messages.
4804
@type messages: C{MessageSet}
4805
@param messages: The identifiers of messages to retrieve information
4809
@param uid: If true, the IDs specified in the query are UIDs;
4810
otherwise they are message sequence IDs.
4812
@rtype: Any iterable of two-tuples of message sequence numbers and
4813
implementors of C{IMessage}.
4816
def store(messages, flags, mode, uid):
4817
"""Set the flags of one or more messages.
4819
@type messages: A MessageSet object with the list of messages requested
4820
@param messages: The identifiers of the messages to set the flags of.
4822
@type flags: sequence of C{str}
4823
@param flags: The flags to set, unset, or add.
4825
@type mode: -1, 0, or 1
4826
@param mode: If mode is -1, these flags should be removed from the
4827
specified messages. If mode is 1, these flags should be added to
4828
the specified messages. If mode is 0, all existing flags should be
4829
cleared and these flags should be added.
4832
@param uid: If true, the IDs specified in the query are UIDs;
4833
otherwise they are message sequence IDs.
4835
@rtype: C{dict} or C{Deferred}
4836
@return: A C{dict} mapping message sequence numbers to sequences of C{str}
4837
representing the flags set on the message after this operation has
4838
been performed, or a C{Deferred} whose callback will be invoked with
4841
@raise ReadOnlyMailbox: Raised if this mailbox is not open for
4845
class ICloseableMailbox(Interface):
4846
"""A supplementary interface for mailboxes which require cleanup on close.
4848
Implementing this interface is optional. If it is implemented, the protocol
4849
code will call the close method defined whenever a mailbox is closed.
4852
"""Close this mailbox.
4854
@return: A C{Deferred} which fires when this mailbox
4855
has been closed, or None if the mailbox can be closed
4859
def _formatHeaders(headers):
4860
hdrs = [': '.join((k.title(), '\r\n'.join(v.splitlines()))) for (k, v)
4861
in headers.iteritems()]
4862
hdrs = '\r\n'.join(hdrs) + '\r\n'
4869
yield m.getSubPart(i)
4874
def iterateInReactor(i):
4875
"""Consume an interator at most a single iteration per reactor iteration.
4877
If the iterator produces a Deferred, the next iteration will not occur
4878
until the Deferred fires, otherwise the next iteration will be taken
4879
in the next reactor iteration.
4882
@return: A deferred which fires (with None) when the iterator is
4883
exhausted or whose errback is called if there is an exception.
4885
from twisted.internet import reactor
4886
d = defer.Deferred()
4890
except StopIteration:
4895
if isinstance(r, defer.Deferred):
4898
reactor.callLater(0, go, r)
4902
class MessageProducer:
4903
CHUNK_SIZE = 2 ** 2 ** 2 ** 2
4905
def __init__(self, msg, buffer = None, scheduler = None):
4906
"""Produce this message.
4908
@param msg: The message I am to produce.
4909
@type msg: L{IMessage}
4911
@param buffer: A buffer to hold the message in. If None, I will
4912
use a L{tempfile.TemporaryFile}.
4913
@type buffer: file-like
4917
buffer = tempfile.TemporaryFile()
4918
self.buffer = buffer
4919
if scheduler is None:
4920
scheduler = iterateInReactor
4921
self.scheduler = scheduler
4922
self.write = self.buffer.write
4924
def beginProducing(self, consumer):
4925
self.consumer = consumer
4926
return self.scheduler(self._produce())
4929
headers = self.msg.getHeaders(True)
4931
if self.msg.isMultipart():
4932
content = headers.get('content-type')
4933
parts = [x.split('=', 1) for x in content.split(';')[1:]]
4934
parts = dict([(k.lower().strip(), v) for (k, v) in parts])
4935
boundary = parts.get('boundary')
4936
if boundary is None:
4938
boundary = '----=_%f_boundary_%f' % (time.time(), random.random())
4939
headers['content-type'] += '; boundary="%s"' % (boundary,)
4941
if boundary.startswith('"') and boundary.endswith('"'):
4942
boundary = boundary[1:-1]
4944
self.write(_formatHeaders(headers))
4946
if self.msg.isMultipart():
4947
for p in subparts(self.msg):
4948
self.write('\r\n--%s\r\n' % (boundary,))
4949
yield MessageProducer(p, self.buffer, self.scheduler
4950
).beginProducing(None
4952
self.write('\r\n--%s--\r\n' % (boundary,))
4954
f = self.msg.getBodyFile()
4956
b = f.read(self.CHUNK_SIZE)
4958
self.buffer.write(b)
4963
self.buffer.seek(0, 0)
4964
yield FileProducer(self.buffer
4965
).beginProducing(self.consumer
4966
).addCallback(lambda _: self
4971
# Response should be a list of fields from the message:
4972
# date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
4975
# from, sender, reply-to, to, cc, and bcc are themselves lists of
4976
# address information:
4977
# personal name, source route, mailbox name, host name
4979
# reply-to and sender must not be None. If not present in a message
4980
# they should be defaulted to the value of the from field.
4982
__str__ = lambda self: 'envelope'
4986
__str__ = lambda self: 'flags'
4989
type = 'internaldate'
4990
__str__ = lambda self: 'internaldate'
4993
type = 'rfc822header'
4994
__str__ = lambda self: 'rfc822.header'
4998
__str__ = lambda self: 'rfc822.text'
5002
__str__ = lambda self: 'rfc822.size'
5006
__str__ = lambda self: 'rfc822'
5010
__str__ = lambda self: 'uid'
5021
partialLength = None
5027
part = '.'.join([str(x + 1) for x in self.part])
5032
base += '[%s%s%s]' % (part, separator, self.header,)
5034
base += '[%s%sTEXT]' % (part, separator)
5036
base += '[%s%sMIME]' % (part, separator)
5038
base += '[%s]' % (part,)
5039
if self.partialBegin is not None:
5040
base += '<%d.%d>' % (self.partialBegin, self.partialLength)
5043
class BodyStructure:
5044
type = 'bodystructure'
5045
__str__ = lambda self: 'bodystructure'
5047
# These three aren't top-level, they don't need type indicators
5059
for f in self.fields:
5064
base += ' (%s)' % ' '.join(fields)
5066
base = '.'.join([str(x + 1) for x in self.part]) + '.' + base
5077
_simple_fetch_att = [
5078
('envelope', Envelope),
5080
('internaldate', InternalDate),
5081
('rfc822.header', RFC822Header),
5082
('rfc822.text', RFC822Text),
5083
('rfc822.size', RFC822Size),
5086
('bodystructure', BodyStructure),
5090
self.state = ['initial']
5094
def parseString(self, s):
5095
s = self.remaining + s
5097
while s or self.state:
5098
# print 'Entering state_' + self.state[-1] + ' with', repr(s)
5099
state = self.state.pop()
5101
used = getattr(self, 'state_' + state)(s)
5103
self.state.append(state)
5106
# print state, 'consumed', repr(s[:used])
5111
def state_initial(self, s):
5112
# In the initial state, the literals "ALL", "FULL", and "FAST"
5113
# are accepted, as is a ( indicating the beginning of a fetch_att
5114
# token, as is the beginning of a fetch_att token.
5119
if l.startswith('all'):
5120
self.result.extend((
5121
self.Flags(), self.InternalDate(),
5122
self.RFC822Size(), self.Envelope()
5125
if l.startswith('full'):
5126
self.result.extend((
5127
self.Flags(), self.InternalDate(),
5128
self.RFC822Size(), self.Envelope(),
5132
if l.startswith('fast'):
5133
self.result.extend((
5134
self.Flags(), self.InternalDate(), self.RFC822Size(),
5138
if l.startswith('('):
5139
self.state.extend(('close_paren', 'maybe_fetch_att', 'fetch_att'))
5142
self.state.append('fetch_att')
5145
def state_close_paren(self, s):
5146
if s.startswith(')'):
5148
raise Exception("Missing )")
5150
def state_whitespace(self, s):
5151
# Eat up all the leading whitespace
5152
if not s or not s[0].isspace():
5153
raise Exception("Whitespace expected, none found")
5155
for i in range(len(s)):
5156
if not s[i].isspace():
5160
def state_maybe_fetch_att(self, s):
5161
if not s.startswith(')'):
5162
self.state.extend(('maybe_fetch_att', 'fetch_att', 'whitespace'))
5165
def state_fetch_att(self, s):
5166
# Allowed fetch_att tokens are "ENVELOPE", "FLAGS", "INTERNALDATE",
5167
# "RFC822", "RFC822.HEADER", "RFC822.SIZE", "RFC822.TEXT", "BODY",
5168
# "BODYSTRUCTURE", "UID",
5169
# "BODY [".PEEK"] [<section>] ["<" <number> "." <nz_number> ">"]
5172
for (name, cls) in self._simple_fetch_att:
5173
if l.startswith(name):
5174
self.result.append(cls())
5178
if l.startswith('body.peek'):
5181
elif l.startswith('body'):
5184
raise Exception("Nothing recognized in fetch_att: %s" % (l,))
5186
self.pending_body = b
5187
self.state.extend(('got_body', 'maybe_partial', 'maybe_section'))
5190
def state_got_body(self, s):
5191
self.result.append(self.pending_body)
5192
del self.pending_body
5195
def state_maybe_section(self, s):
5196
if not s.startswith("["):
5199
self.state.extend(('section', 'part_number'))
5202
_partExpr = re.compile(r'(\d+(?:\.\d+)*)\.?')
5203
def state_part_number(self, s):
5204
m = self._partExpr.match(s)
5206
self.parts = [int(p) - 1 for p in m.groups()[0].split('.')]
5212
def state_section(self, s):
5213
# Grab "HEADER]" or "HEADER.FIELDS (Header list)]" or
5214
# "HEADER.FIELDS.NOT (Header list)]" or "TEXT]" or "MIME]" or
5219
if l.startswith(']'):
5220
self.pending_body.empty = True
5222
elif l.startswith('header]'):
5223
h = self.pending_body.header = self.Header()
5227
elif l.startswith('text]'):
5228
self.pending_body.text = self.Text()
5230
elif l.startswith('mime]'):
5231
self.pending_body.mime = self.MIME()
5235
if l.startswith('header.fields.not'):
5238
elif l.startswith('header.fields'):
5241
raise Exception("Unhandled section contents: %r" % (l,))
5243
self.pending_body.header = h
5244
self.state.extend(('finish_section', 'header_list', 'whitespace'))
5245
self.pending_body.part = tuple(self.parts)
5249
def state_finish_section(self, s):
5250
if not s.startswith(']'):
5251
raise Exception("section must end with ]")
5254
def state_header_list(self, s):
5255
if not s.startswith('('):
5256
raise Exception("Header list must begin with (")
5259
raise Exception("Header list must end with )")
5261
headers = s[1:end].split()
5262
self.pending_body.header.fields = map(str.upper, headers)
5265
def state_maybe_partial(self, s):
5266
# Grab <number.number> or nothing at all
5267
if not s.startswith('<'):
5271
raise Exception("Found < but not >")
5274
parts = partial.split('.', 1)
5276
raise Exception("Partial specification did not include two .-delimited integers")
5277
begin, length = map(int, parts)
5278
self.pending_body.partialBegin = begin
5279
self.pending_body.partialLength = length
5284
CHUNK_SIZE = 2 ** 2 ** 2 ** 2
5288
def __init__(self, f):
5291
def beginProducing(self, consumer):
5292
self.consumer = consumer
5293
self.produce = consumer.write
5294
d = self._onDone = defer.Deferred()
5295
self.consumer.registerProducer(self, False)
5298
def resumeProducing(self):
5301
b = '{%d}\r\n' % self._size()
5302
self.firstWrite = False
5305
b = b + self.f.read(self.CHUNK_SIZE)
5307
self.consumer.unregisterProducer()
5308
self._onDone.callback(self)
5309
self._onDone = self.f = self.consumer = None
5313
def pauseProducing(self):
5316
def stopProducing(self):
5327
# XXX - This may require localization :(
5329
'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct',
5330
'nov', 'dec', 'january', 'february', 'march', 'april', 'may', 'june',
5331
'july', 'august', 'september', 'october', 'november', 'december'
5334
'day': r"(?P<day>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
5335
'mon': r"(?P<mon>\w+)",
5336
'year': r"(?P<year>\d\d\d\d)"
5338
m = re.match('%(day)s-%(mon)s-%(year)s' % expr, s)
5340
raise ValueError, "Cannot parse time string %r" % (s,)
5343
d['mon'] = 1 + (months.index(d['mon'].lower()) % 12)
5344
d['year'] = int(d['year'])
5345
d['day'] = int(d['day'])
5347
raise ValueError, "Cannot parse time string %r" % (s,)
5349
return time.struct_time(
5350
(d['year'], d['mon'], d['day'], 0, 0, 0, -1, -1, -1)
5354
def modified_base64(s):
5355
s_utf7 = s.encode('utf-7')
5356
return s_utf7[1:-1].replace('/', ',')
5358
def modified_unbase64(s):
5359
s_utf7 = '+' + s.replace(',', '/') + '-'
5360
return s_utf7.decode('utf-7')
5366
if ord(c) in (range(0x20, 0x26) + range(0x27, 0x7f)):
5368
r.extend(['&', modified_base64(''.join(_in)), '-'])
5373
r.extend(['&', modified_base64(''.join(_in)), '-'])
5379
r.extend(['&', modified_base64(''.join(_in)), '-'])
5380
return (''.join(r), len(s))
5386
if c == '&' and not decode:
5388
elif c == '-' and decode:
5389
if len(decode) == 1:
5392
r.append(modified_unbase64(''.join(decode[1:])))
5399
r.append(modified_unbase64(''.join(decode[1:])))
5400
return (''.join(r), len(s))
5402
class StreamReader(codecs.StreamReader):
5403
def decode(self, s, errors='strict'):
5406
class StreamWriter(codecs.StreamWriter):
5407
def decode(self, s, errors='strict'):
5410
def imap4_utf_7(name):
5411
if name == 'imap4-utf-7':
5412
return (encoder, decoder, StreamReader, StreamWriter)
5413
codecs.register(imap4_utf_7)
5417
'IMAP4Server', 'IMAP4Client',
5420
'IMailboxListener', 'IClientAuthentication', 'IAccount', 'IMailbox',
5421
'INamespacePresenter', 'ICloseableMailbox', 'IMailboxInfo',
5422
'IMessage', 'IMessageCopier', 'IMessageFile', 'ISearchableMailbox',
5425
'IMAP4Exception', 'IllegalClientResponse', 'IllegalOperation',
5426
'IllegalMailboxEncoding', 'UnhandledResponse', 'NegativeResponse',
5427
'NoSupportedAuthentication', 'IllegalServerResponse',
5428
'IllegalIdentifierError', 'IllegalQueryError', 'MismatchedNesting',
5429
'MismatchedQuoting', 'MailboxException', 'MailboxCollision',
5430
'NoSuchMailbox', 'ReadOnlyMailbox',
5433
'CramMD5ClientAuthenticator', 'PLAINAuthenticator', 'LOGINAuthenticator',
5434
'PLAINCredentials', 'LOGINCredentials',
5436
# Simple query interface
5437
'Query', 'Not', 'Or',
5441
'statusRequestHelper',