1
# -*- test-case-name: twisted.test.test_imap -*-
2
# Twisted, the Framework of Your Internet
3
# Copyright (C) 2001 Matthew W. Lefkowitz
5
# This library is free software; you can redistribute it and/or
6
# modify it under the terms of version 2.1 of the GNU Lesser General Public
7
# License as published by the Free Software Foundation.
9
# This library is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
# Lesser General Public License for more details.
14
# You should have received a copy of the GNU Lesser General Public
15
# License along with this library; if not, write to the Free Software
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19
An IMAP4 protocol implementation
21
API Stability: Semi-stable
23
@author: U{Jp Calderone<mailto:exarkun@twistedmatrix.com>}
26
Suspend idle timeout while server is processing
27
Use an async message parser instead of buffering in memory
28
Figure out a way to not queue multi-message client requests (Flow? A simple callback?)
29
Clarify some API docs (Query, etc)
30
Make APPEND recognize (again) non-existent mailboxes before accepting the literal
33
from __future__ import nested_scopes
34
from __future__ import generators
36
from twisted.protocols import basic
37
from twisted.protocols import policies
38
from twisted.internet import defer
39
from twisted.internet import error
40
from twisted.internet.defer import maybeDeferred
41
from twisted.python import log, components, util, failure, text
42
from twisted.cred import perspective
43
from twisted.python.components import implements
44
from twisted.internet import interfaces
46
from twisted import cred
47
import twisted.cred.error
48
import twisted.cred.credentials
66
import cStringIO as StringIO
70
infrangeobject = xrange(sys.maxint)
72
class MessageSet(object):
74
Essentially an infinite bitfield, with some extra features.
76
@type getnext: Function taking C{int} returning C{int}
77
@ivar getnext: A function that returns the next message number,
78
used when iterating through the MessageSet. By default, a function
79
returning the next integer is supplied, but as this can be rather
80
inefficient for sparse UID iterations, it is recommended to supply
81
one when messages are requested by UID. The argument is provided
82
as a hint to the implementation and may be ignored if it makes sense
83
to do so (eg, if an iterator is being used that maintains its own
84
state, it is guaranteed that it will not be called out-of-order).
88
def __init__(self, start=_empty, end=_empty):
90
Create a new MessageSet()
92
@type start: Optional C{int}
93
@param start: Start of range, or only message number
95
@type end: Optional C{int}
96
@param end: End of range.
98
self._last = self._empty # Last message/UID in use
99
self.ranges = [] # List of ranges included
100
self.getnext = lambda x: x+1 # A function which will return the next
101
# message id. Handy for UID requests.
103
if start is self._empty:
106
if isinstance(start, types.ListType):
107
self.ranges = start[:]
114
def _setLast(self,value):
115
if self._last is not self._empty:
116
raise ValueError("last already set")
119
for i,(l,h) in zip(infrangeobject,self.ranges):
121
break # There are no more Nones after this
127
self.ranges[i] = (l,h)
135
"Highest" message number, refered to by "*".
136
Must be set before attempting to use the MessageSet.
138
return _getLast, _setLast, None, doc
139
last = property(*last())
141
def add(self, start, end=_empty):
146
@param start: Start of range, or only message number
148
@type end: Optional C{int}
149
@param end: End of range.
151
if end is self._empty:
154
if self._last is not self._empty:
161
# Try to keep in low, high order if possible
162
# (But we don't know what None means, this will keep
163
# None at the start of the ranges list)
164
start, end = end, start
166
self.ranges.append((start,end))
169
def __add__(self, other):
170
if isinstance(other, MessageSet):
171
ranges = self.ranges + other.ranges
172
return MessageSet(ranges)
174
res = MessageSet(self.ranges)
181
def extend(self, other):
182
if isinstance(other, MessageSet):
183
self.ranges.extend(other.ranges)
195
Clean ranges list, combining adjacent ranges
200
oldl, oldh = None, None
201
for i,(l,h) in zip(infrangeobject,self.ranges):
204
# l is >= oldl and h is >= oldh due to sort()
205
if oldl is not None and l <= oldh+1:
208
self.ranges[i-1] = None
209
self.ranges[i] = (l,h)
213
self.ranges = filter(None, self.ranges)
215
def __contains__(self, value):
217
May raise TypeError if we encounter unknown "high" values
219
for l,h in self.ranges:
222
"Can't determine membership; last value not set")
229
for l,h in self.ranges:
230
l = self.getnext(l-1)
238
if self.ranges and self.ranges[0][0] is None:
239
raise TypeError("Can't iterate; last value not set")
241
return self._iterator()
245
for l, h in self.ranges:
247
raise TypeError("Can't size object; last value not set")
254
for low, high in self.ranges:
261
p.append('%d:*' % (high,))
263
p.append('%d:%d' % (low, high))
267
return '<MessageSet %s>' % (str(self),)
269
def __eq__(self, other):
270
if isinstance(other, MessageSet):
271
return self.ranges == other.ranges
276
def __init__(self, size, defered):
281
def write(self, data):
282
self.size -= len(data)
285
self.data.append(data)
288
data, passon = data[:self.size], data[self.size:]
292
self.data.append(data)
295
def callback(self, line):
297
Call defered with data and rest of line
299
self.defer.callback((''.join(self.data), line))
302
_memoryFileLimit = 1024 * 1024 * 10
304
def __init__(self, size, defered):
307
if size > self._memoryFileLimit:
308
self.data = tempfile.TemporaryFile()
310
self.data = StringIO.StringIO()
312
def write(self, data):
313
self.size -= len(data)
316
self.data.write(data)
319
data, passon = data[:self.size], data[self.size:]
323
self.data.write(data)
326
def callback(self, line):
328
Call defered with data and rest of line
331
self.defer.callback((self.data, line))
335
"""Buffer up a bunch of writes before sending them all to a transport at once.
337
def __init__(self, transport, size=8192):
338
self.bufferSize = size
339
self.transport = transport
344
self._length += len(s)
345
self._writes.append(s)
346
if self._length > self.bufferSize:
351
self.transport.writeSequence(self._writes)
357
_1_RESPONSES = ('CAPABILITY', 'FLAGS', 'LIST', 'LSUB', 'STATUS', 'SEARCH', 'NAMESPACE')
358
_2_RESPONSES = ('EXISTS', 'EXPUNGE', 'FETCH', 'RECENT')
359
_OK_RESPONSES = ('UIDVALIDITY', 'READ-WRITE', 'READ-ONLY', 'UIDNEXT', 'PERMANENTFLAGS')
362
def __init__(self, command, args=None, wantResponse=(),
363
continuation=None, *contArgs, **contKw):
364
self.command = command
366
self.wantResponse = wantResponse
367
self.continuation = lambda x: continuation(x, *contArgs, **contKw)
370
def format(self, tag):
371
if self.args is None:
372
return ' '.join((tag, self.command))
373
return ' '.join((tag, self.command, self.args))
375
def finish(self, lastLine, unusedCallback):
379
names = parseNestedParens(L)
381
if (N >= 1 and names[0] in self._1_RESPONSES or
382
N >= 2 and names[1] in self._2_RESPONSES or
383
N >= 2 and names[0] == 'OK' and isinstance(names[1], types.ListType) and names[1][0] in self._OK_RESPONSES):
387
self.defer.callback((send, lastLine))
389
unusedCallback(unuse)
391
class LOGINCredentials(cred.credentials.UsernamePassword):
393
self.challenges = ['Password\0', 'User Name\0']
394
self.responses = ['password', 'username']
395
cred.credentials.UsernamePassword.__init__(self, None, None)
397
def getChallenge(self):
398
return self.challenges.pop()
400
def setResponse(self, response):
401
setattr(self, self.responses.pop(), response)
403
def moreChallenges(self):
404
return bool(self.challenges)
406
class PLAINCredentials(cred.credentials.UsernamePassword):
408
cred.credentials.UsernamePassword.__init__(self, None, None)
410
def getChallenge(self):
413
def setResponse(self, response):
414
parts = response[:-1].split('\0', 1)
416
raise IllegalClientResponse("Malformed Response - wrong number of parts")
417
self.username, self.password = parts
419
def moreChallenges(self):
422
class IMAP4Exception(Exception):
423
def __init__(self, *args):
424
Exception.__init__(self, *args)
426
class IllegalClientResponse(IMAP4Exception): pass
428
class IllegalOperation(IMAP4Exception): pass
430
class IllegalMailboxEncoding(IMAP4Exception): pass
432
class IMailboxListener(components.Interface):
433
"""Interface for objects interested in mailbox events"""
435
def modeChanged(self, writeable):
436
"""Indicates that the write status of a mailbox has changed.
438
@type writeable: C{bool}
439
@param writeable: A true value if write is now allowed, false
443
def flagsChanged(self, newFlags):
444
"""Indicates that the flags of one or more messages have changed.
446
@type newFlags: C{dict}
447
@param newFlags: A mapping of message identifiers to tuples of flags
448
now set on that message.
451
def newMessages(self, exists, recent):
452
"""Indicates that the number of messages in a mailbox has changed.
454
@type exists: C{int} or C{None}
455
@param exists: The total number of messages now in this mailbox.
456
If the total number of messages has not changed, this should be
460
@param recent: The number of messages now flagged \\Recent.
461
If the number of recent messages has not changed, this should be
465
class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin):
467
Protocol implementation for an IMAP4rev1 server.
469
The server can be in any of four states:
475
__implements__ = (IMailboxListener,)
477
# Identifier for this server software
478
IDENT = 'Twisted IMAP4rev1 Ready'
480
# Number of seconds before idle timeout
481
# Initially 1 minute. Raised to 30 minutes after login.
484
POSTAUTH_TIMEOUT = 60 * 30
486
# Whether STARTTLS has been issued successfully yet or not.
489
# Whether our transport supports TLS
492
# Mapping of tags to commands we have received
495
# The object which will handle logins for us
498
# The account object for this connection
504
# The currently selected mailbox
507
# Command data to be processed when literal data is received
508
_pendingLiteral = None
510
# Maximum length to accept for a "short" string literal
511
_literalStringLimit = 4096
513
# IChallengeResponse factories for AUTHENTICATE command
518
parseState = 'command'
520
def __init__(self, chal = None, contextFactory = None):
523
self.challengers = chal
524
self.ctx = contextFactory
525
self._queuedAsync = []
527
def capabilities(self):
528
cap = {'AUTH': self.challengers.keys()}
529
if self.ctx and self.canStartTLS:
530
if not self.startedTLS and self.transport.getHost()[0] != 'SSL':
531
cap['LOGINDISABLED'] = None
532
cap['STARTTLS'] = None
533
cap['NAMESPACE'] = None
537
def connectionMade(self):
539
self.canStartTLS = interfaces.ITLSTransport(self.transport, default=None) is not None
540
self.setTimeout(self.timeOut)
541
self.sendServerGreeting()
543
def connectionLost(self, reason):
544
self.setTimeout(None)
547
self._onLogout = None
549
def timeoutConnection(self):
550
self.sendLine('* BYE Autologout; connection idle too long')
551
self.transport.loseConnection()
553
self.mbox.removeListener(self)
554
cmbx = ICloseableMailbox(self.mbox, default=None)
556
maybeDeferred(cmbx.close).addErrback(log.err)
558
self.state = 'timeout'
560
def rawDataReceived(self, data):
562
passon = self._pendingLiteral.write(data)
563
if passon is not None:
564
self.setLineMode(passon)
566
# Avoid processing commands while buffers are being dumped to
571
commands = self.blocked
573
while commands and self.blocked is None:
574
self.lineReceived(commands.pop(0))
575
if self.blocked is not None:
576
self.blocked.extend(commands)
578
# def sendLine(self, line):
579
# print 'C:', repr(line)
580
# return basic.LineReceiver.sendLine(self, line)
582
def lineReceived(self, line):
583
# print 'S:', repr(line)
586
if self.blocked is not None:
587
self.blocked.append(line)
590
f = getattr(self, 'parse_' + self.parseState)
594
self.sendUntaggedResponse('BAD Server error: ' + str(e))
597
def parse_command(self, line):
598
args = line.split(None, 2)
601
tag, cmd, rest = args
606
self.sendBadResponse(tag, 'Missing command')
609
self.sendBadResponse(None, 'Null command')
614
return self.dispatchCommand(tag, cmd, rest)
615
except IllegalClientResponse, e:
616
self.sendBadResponse(tag, 'Illegal syntax: ' + str(e))
617
except IllegalOperation, e:
618
self.sendNegativeResponse(tag, 'Illegal operation: ' + str(e))
619
except IllegalMailboxEncoding, e:
620
self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + str(e))
622
def parse_pending(self, line):
623
d = self._pendingLiteral
624
self._pendingLiteral = None
625
self.parseState = 'command'
628
def dispatchCommand(self, tag, cmd, rest, uid=None):
629
f = self.lookupCommand(cmd)
633
self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid)
635
self.sendBadResponse(tag, 'Unsupported command')
637
def lookupCommand(self, cmd):
638
return getattr(self, '_'.join((self.state, cmd.upper())), None)
640
def __doCommand(self, tag, handler, args, parseargs, line, uid):
641
for (i, arg) in zip(infrangeobject, parseargs):
643
parseargs = parseargs[i+1:]
644
maybeDeferred(arg, self, line).addCallback(
645
self.__cbDispatch, tag, handler, args,
646
parseargs, uid).addErrback(self.__ebDispatch, tag)
653
raise IllegalClientResponse("Too many arguments for command: " + repr(line))
656
handler(uid=uid, *args)
660
def __cbDispatch(self, (arg, rest), tag, fn, args, parseargs, uid):
662
self.__doCommand(tag, fn, args, parseargs, rest, uid)
664
def __ebDispatch(self, failure, tag):
665
if failure.check(IllegalClientResponse):
666
self.sendBadResponse(tag, 'Illegal syntax: ' + str(failure.value))
667
elif failure.check(IllegalOperation):
668
self.sendNegativeResponse(tag, 'Illegal operation: ' +
670
elif failure.check(IllegalMailboxEncoding):
671
self.sendNegativeResponse(tag, 'Illegal mailbox name: ' +
674
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
677
def _stringLiteral(self, size):
678
if size > self._literalStringLimit:
679
raise IllegalClientResponse(
680
"Literal too long! I accept at most %d octets" %
681
(self._literalStringLimit,))
683
self.parseState = 'pending'
684
self._pendingLiteral = LiteralString(size, d)
685
self.sendContinuationRequest('Ready for %d octets of text' % size)
689
def _fileLiteral(self, size):
691
self.parseState = 'pending'
692
self._pendingLiteral = LiteralFile(size, d)
693
self.sendContinuationRequest('Ready for %d octets of data' % size)
697
def arg_astring(self, line):
699
Parse an astring from the line, return (arg, rest), possibly
700
via a deferred (to handle literals)
703
raise IllegalClientResponse("Missing argument")
705
arg, rest = None, None
708
spam, arg, rest = line.split('"',2)
709
rest = rest[1:] # Strip space
711
raise IllegalClientResponse("Unmatched quotes")
715
raise IllegalClientResponse("Malformed literal")
717
size = int(line[1:-1])
719
raise IllegalClientResponse("Bad literal size: " + line[1:-1])
720
d = self._stringLiteral(size)
722
arg = line.split(' ',1)
726
return d or (arg, rest)
728
# ATOM: Any CHAR except ( ) { % * " \ ] CTL SP (CHAR is 7bit)
729
atomre = re.compile(r'(?P<atom>[^\](){%*"\\\x00-\x20\x80-\xff]+)( (?P<rest>.*$)|$)')
731
def arg_atom(self, line):
733
Parse an atom from the line
736
raise IllegalClientResponse("Missing argument")
737
m = self.atomre.match(line)
739
return m.group('atom'), m.group('rest')
741
raise IllegalClientResponse("Malformed ATOM")
743
def arg_plist(self, line):
745
Parse a (non-nested) parenthesised list from the line
748
raise IllegalClientResponse("Missing argument")
751
raise IllegalClientResponse("Missing parenthesis")
756
raise IllegalClientResponse("Mismatched parenthesis")
758
return (parseNestedParens(line[1:i],0), line[i+2:])
760
def arg_literal(self, line):
762
Parse a literal from the line
765
raise IllegalClientResponse("Missing argument")
768
raise IllegalClientResponse("Missing literal")
771
raise IllegalClientResponse("Malformed literal")
774
size = int(line[1:-1])
776
raise IllegalClientResponse("Bad literal size: " + line[1:-1])
778
return self._fileLiteral(size)
780
def arg_searchkeys(self, line):
784
query = parseNestedParens(line)
785
# XXX Should really use list of search terms and parse into
790
def arg_seqset(self, line):
795
arg = line.split(' ',1)
801
return (parseIdList(arg), rest)
802
except IllegalIdentifierError, e:
803
raise IllegalClientResponse("Bad message number " + str(e))
805
def arg_fetchatt(self, line):
811
return (p.result, '')
813
def arg_flaglist(self, line):
815
Flag part of store-att-flag
820
raise IllegalClientResponse("Mismatched parenthesis")
824
m = self.atomre.search(line)
826
raise IllegalClientResponse("Malformed flag")
827
if line[0] == '\\' and m.start() == 1:
828
flags.append('\\' + m.group('atom'))
830
flags.append(m.group('atom'))
832
raise IllegalClientResponse("Malformed flag")
833
line = m.group('rest')
837
def arg_line(self, line):
839
Command line of UID command
843
def opt_plist(self, line):
845
Optional parenthesised list
847
if line.startswith('('):
848
return self.arg_plist(line)
852
def opt_datetime(self, line):
854
Optional date-time string
856
if line.startswith('"'):
858
spam, date, rest = line.split('"',2)
860
raise IllegalClientResponse("Malformed date-time")
861
return (date, rest[1:])
865
def opt_charset(self, line):
867
Optional charset of SEARCH command
869
if line[:7].upper() == 'CHARSET':
870
arg = line.split(' ',2)
872
raise IllegalClientResponse("Missing charset identifier")
875
spam, arg, rest = arg
880
def sendServerGreeting(self):
881
msg = '[CAPABILITY %s] %s' % (' '.join(self.listCapabilities()), self.IDENT)
882
self.sendPositiveResponse(message=msg)
884
def sendBadResponse(self, tag = None, message = ''):
885
self._respond('BAD', tag, message)
887
def sendPositiveResponse(self, tag = None, message = ''):
888
self._respond('OK', tag, message)
890
def sendNegativeResponse(self, tag = None, message = ''):
891
self._respond('NO', tag, message)
893
def sendUntaggedResponse(self, message, async=False):
894
if not async or (self.blocked is None):
895
self._respond(message, None, None)
897
self._queuedAsync.append(message)
899
def sendContinuationRequest(self, msg = 'Ready for additional command text'):
901
self.sendLine('+ ' + msg)
905
def _respond(self, state, tag, message):
906
if state in ('OK', 'NO', 'BAD') and self._queuedAsync:
907
lines = self._queuedAsync
908
self._queuedAsync = []
910
self._respond(msg, None, None)
914
self.sendLine(' '.join((tag, state, message)))
916
self.sendLine(' '.join((tag, state)))
918
def listCapabilities(self):
920
for c, v in self.capabilities().iteritems():
924
caps.extend([('%s=%s' % (c, cap)) for cap in v])
927
def do_CAPABILITY(self, tag):
928
self.sendUntaggedResponse('CAPABILITY ' + ' '.join(self.listCapabilities()))
929
self.sendPositiveResponse(tag, 'CAPABILITY completed')
931
unauth_CAPABILITY = (do_CAPABILITY,)
932
auth_CAPABILITY = unauth_CAPABILITY
933
select_CAPABILITY = unauth_CAPABILITY
934
logout_CAPABILITY = unauth_CAPABILITY
936
def do_LOGOUT(self, tag):
937
self.sendUntaggedResponse('BYE Nice talking to you')
938
self.sendPositiveResponse(tag, 'LOGOUT successful')
939
self.transport.loseConnection()
941
unauth_LOGOUT = (do_LOGOUT,)
942
auth_LOGOUT = unauth_LOGOUT
943
select_LOGOUT = unauth_LOGOUT
944
logout_LOGOUT = unauth_LOGOUT
946
def do_NOOP(self, tag):
947
self.sendPositiveResponse(tag, 'NOOP No operation performed')
949
unauth_NOOP = (do_NOOP,)
950
auth_NOOP = unauth_NOOP
951
select_NOOP = unauth_NOOP
952
logout_NOOP = unauth_NOOP
954
def do_AUTHENTICATE(self, tag, args):
955
args = args.upper().strip()
956
if args not in self.challengers:
957
self.sendNegativeResponse(tag, 'AUTHENTICATE method unsupported')
959
self.authenticate(self.challengers[args](), tag)
961
unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom)
963
def authenticate(self, chal, tag):
964
if self.portal is None:
965
self.sendNegativeResponse(tag, 'Temporary authentication failure')
968
self._setupChallenge(chal, tag)
970
def _setupChallenge(self, chal, tag):
972
challenge = chal.getChallenge()
974
self.sendBadResponse(tag, 'Server error: ' + str(e))
976
coded = base64.encodestring(challenge)[:-1]
977
self.parseState = 'pending'
978
self._pendingLiteral = defer.Deferred()
979
self.sendContinuationRequest(coded)
980
self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag)
981
self._pendingLiteral.addErrback(self.__ebAuthChunk, tag)
983
def __cbAuthChunk(self, result, chal, tag):
985
uncoded = base64.decodestring(result)
986
except binascii.Error:
987
raise IllegalClientResponse("Malformed Response - not base64")
989
chal.setResponse(uncoded)
990
if chal.moreChallenges():
991
self._setupChallenge(chal, tag)
993
self.portal.login(chal, None, IAccount).addCallbacks(
996
(tag,), None, (tag,), None
999
def __cbAuthResp(self, (iface, avatar, logout), tag):
1000
assert iface is IAccount, "IAccount is the only supported interface"
1001
self.account = avatar
1003
self._onLogout = logout
1004
self.sendPositiveResponse(tag, 'Authentication successful')
1005
self.setTimeout(self.POSTAUTH_TIMEOUT)
1007
def __ebAuthResp(self, failure, tag):
1008
if failure.check(cred.error.UnauthorizedLogin):
1009
self.sendNegativeResponse(tag, 'Authentication failed: unauthorized')
1010
elif failure.check(cred.error.UnhandledCredentials):
1011
self.sendNegativeResponse(tag, 'Authentication failed: server misconfigured')
1013
self.sendBadResponse(tag, 'Server error: login failed unexpectedly')
1016
def __ebAuthChunk(self, failure, tag):
1017
self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.value))
1019
def do_STARTTLS(self, tag):
1021
self.sendNegativeResponse(tag, 'TLS already negotiated')
1022
elif self.ctx and self.canStartTLS:
1023
self.sendPositiveResponse(tag, 'Begin TLS negotiation now')
1024
self.transport.startTLS(self.ctx)
1025
self.startedTLS = True
1026
self.challengers = self.challengers.copy()
1027
if 'LOGIN' not in self.challengers:
1028
self.challengers['LOGIN'] = LOGINCredentials
1029
if 'PLAIN' not in self.challengers:
1030
self.challengers['PLAIN'] = PLAINCredentials
1032
self.sendNegativeResponse(tag, 'TLS not available')
1034
unauth_STARTTLS = (do_STARTTLS,)
1036
def do_LOGIN(self, tag, user, passwd):
1037
if 'LOGINDISABLED' in self.capabilities():
1038
self.sendBadResponse(tag, 'LOGIN is disabled before STARTTLS')
1041
maybeDeferred(self.authenticateLogin, user, passwd
1042
).addCallback(self.__cbLogin, tag
1043
).addErrback(self.__ebLogin, tag
1046
unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring)
1048
def authenticateLogin(self, user, passwd):
1049
"""Lookup the account associated with the given parameters
1051
Override this method to define the desired authentication behavior.
1053
The default behavior is to defer authentication to C{self.portal}
1054
if it is not None, or to deny the login otherwise.
1057
@param user: The username to lookup
1059
@type passwd: C{str}
1060
@param passwd: The password to login with
1063
return self.portal.login(
1064
cred.credentials.UsernamePassword(user, passwd),
1067
raise cred.error.UnauthorizedLogin()
1069
def __cbLogin(self, (iface, avatar, logout), tag):
1070
if iface is not IAccount:
1071
self.sendBadResponse(tag, 'Server error: login returned unexpected value')
1072
log.err("__cbLogin called with %r, IAccount expected" % (iface,))
1074
self.account = avatar
1075
self._onLogout = logout
1076
self.sendPositiveResponse(tag, 'LOGIN succeeded')
1078
self.setTimeout(self.POSTAUTH_TIMEOUT)
1080
def __ebLogin(self, failure, tag):
1081
if failure.check(cred.error.UnauthorizedLogin):
1082
self.sendNegativeResponse(tag, 'LOGIN failed')
1084
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1087
def do_NAMESPACE(self, tag):
1088
personal = public = shared = None
1089
np = INamespacePresenter(self.account, default=None)
1091
personal = np.getPersonalNamespaces()
1092
public = np.getSharedNamespaces()
1093
shared = np.getSharedNamespaces()
1094
self.sendUntaggedResponse('NAMESPACE ' + collapseNestedLists([personal, public, shared]))
1095
self.sendPositiveResponse(tag, "NAMESPACE command completed")
1097
auth_NAMESPACE = (do_NAMESPACE,)
1098
select_NAMESPACE = auth_NAMESPACE
1100
def _parseMbox(self, name):
1102
return name.decode('imap4-utf-7')
1105
raise IllegalMailboxEncoding(name)
1107
def _selectWork(self, tag, name, rw, cmdName):
1109
self.mbox.removeListener(self)
1110
cmbx = ICloseableMailbox(self.mbox, default=None)
1111
if cmbx is not None:
1112
maybeDeferred(cmbx.close).addErrback(log.err)
1116
name = self._parseMbox(name)
1117
maybeDeferred(self.account.select, self._parseMbox(name), rw
1118
).addCallback(self._cbSelectWork, cmdName, tag
1119
).addErrback(self._ebSelectWork, cmdName, tag
1122
def _ebSelectWork(self, failure, cmdName, tag):
1123
self.sendBadResponse(tag, "%s failed: Server error" % (cmdName,))
1126
def _cbSelectWork(self, mbox, cmdName, tag):
1128
self.sendNegativeResponse(tag, 'No such mailbox')
1130
if '\\noselect' in [s.lower() for s in mbox.getFlags()]:
1131
self.sendNegativeResponse(tag, 'Mailbox cannot be selected')
1134
flags = mbox.getFlags()
1135
self.sendUntaggedResponse(str(mbox.getMessageCount()) + ' EXISTS')
1136
self.sendUntaggedResponse(str(mbox.getRecentCount()) + ' RECENT')
1137
self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags))
1138
self.sendPositiveResponse(None, '[UIDVALIDITY %d]' % mbox.getUIDValidity())
1140
s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY'
1141
mbox.addListener(self)
1142
self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName))
1143
self.state = 'select'
1146
auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' )
1147
select_SELECT = auth_SELECT
1149
auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' )
1150
select_EXAMINE = auth_EXAMINE
1153
def do_IDLE(self, tag):
1154
self.sendContinuationRequest(None)
1156
self.lastState = self.parseState
1157
self.parseState = 'idle'
1159
def parse_idle(self, *args):
1160
self.parseState = self.lastState
1162
self.sendPositiveResponse(self.parseTag, "IDLE terminated")
1165
select_IDLE = ( do_IDLE, )
1166
auth_IDLE = select_IDLE
1169
def do_CREATE(self, tag, name):
1170
name = self._parseMbox(name)
1172
result = self.account.create(name)
1173
except MailboxException, c:
1174
self.sendNegativeResponse(tag, str(c))
1176
self.sendBadResponse(tag, "Server error encountered while creating mailbox")
1180
self.sendPositiveResponse(tag, 'Mailbox created')
1182
self.sendNegativeResponse(tag, 'Mailbox not created')
1184
auth_CREATE = (do_CREATE, arg_astring)
1185
select_CREATE = auth_CREATE
1187
def do_DELETE(self, tag, name):
1188
name = self._parseMbox(name)
1189
if name.lower() == 'inbox':
1190
self.sendNegativeResponse(tag, 'You cannot delete the inbox')
1193
self.account.delete(name)
1194
except MailboxException, m:
1195
self.sendNegativeResponse(tag, str(m))
1197
self.sendBadResponse(tag, "Server error encountered while deleting mailbox")
1200
self.sendPositiveResponse(tag, 'Mailbox deleted')
1202
auth_DELETE = (do_DELETE, arg_astring)
1203
select_DELETE = auth_DELETE
1205
def do_RENAME(self, tag, oldname, newname):
1206
oldname, newname = [self._parseMbox(n) for n in oldname, newname]
1207
if oldname.lower() == 'inbox' or newname.lower() == 'inbox':
1208
self.sendNegativeResponse(tag, 'You cannot rename the inbox, or rename another mailbox to inbox.')
1211
self.account.rename(oldname, newname)
1213
self.sendBadResponse(tag, 'Invalid command syntax')
1214
except MailboxException, m:
1215
self.sendNegativeResponse(tag, str(m))
1217
self.sendBadResponse(tag, "Server error encountered while renaming mailbox")
1220
self.sendPositiveResponse(tag, 'Mailbox renamed')
1222
auth_RENAME = (do_RENAME, arg_astring, arg_astring)
1223
select_RENAME = auth_RENAME
1225
def do_SUBSCRIBE(self, tag, name):
1226
name = self._parseMbox(name)
1228
self.account.subscribe(name)
1229
except MailboxException, m:
1230
self.sendNegativeResponse(tag, str(m))
1232
self.sendBadResponse(tag, "Server error encountered while subscribing to mailbox")
1235
self.sendPositiveResponse(tag, 'Subscribed')
1237
auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring)
1238
select_SUBSCRIBE = auth_SUBSCRIBE
1240
def do_UNSUBSCRIBE(self, tag, name):
1241
name = self._parseMbox(name)
1243
self.account.unsubscribe(name)
1244
except MailboxException, m:
1245
self.sendNegativeResponse(tag, str(m))
1247
self.sendBadResponse(tag, "Server error encountered while unsubscribing from mailbox")
1250
self.sendPositiveResponse(tag, 'Unsubscribed')
1252
auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring)
1253
select_UNSUBSCRIBE = auth_UNSUBSCRIBE
1255
def _listWork(self, tag, ref, mbox, sub, cmdName):
1256
mbox = self._parseMbox(mbox)
1257
maybeDeferred(self.account.listMailboxes, ref, mbox
1258
).addCallback(self._cbListWork, tag, sub, cmdName
1259
).addErrback(self._ebListWork, tag
1262
def _cbListWork(self, mailboxes, tag, sub, cmdName):
1263
for (name, box) in mailboxes:
1264
if not sub or self.account.isSubscribed(name):
1265
flags = box.getFlags()
1266
delim = box.getHierarchicalDelimiter()
1267
resp = (DontQuoteMe(cmdName), map(DontQuoteMe, flags), delim, name)
1268
self.sendUntaggedResponse(collapseNestedLists(resp))
1269
self.sendPositiveResponse(tag, '%s completed' % (cmdName,))
1271
def _ebListWork(self, failure, tag):
1272
self.sendBadResponse(tag, "Server error encountered while listing mailboxes.")
1275
auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST')
1276
select_LIST = auth_LIST
1278
auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB')
1279
select_LSUB = auth_LSUB
1281
def do_STATUS(self, tag, mailbox, names):
1282
mailbox = self._parseMbox(mailbox)
1283
maybeDeferred(self.account.select, mailbox, 0
1284
).addCallback(self._cbStatusGotMailbox, tag, mailbox, names
1285
).addErrback(self._ebStatusGotMailbox, tag
1288
def _cbStatusGotMailbox(self, mbox, tag, mailbox, names):
1290
maybeDeferred(mbox.requestStatus, names).addCallbacks(
1291
self.__cbStatus, self.__ebStatus,
1292
(tag, mailbox), None, (tag, mailbox), None
1295
self.sendNegativeResponse(tag, "Could not open mailbox")
1297
def _ebStatusGotMailbox(self, failure, tag):
1298
self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
1301
auth_STATUS = (do_STATUS, arg_astring, arg_plist)
1302
select_STATUS = auth_STATUS
1304
def __cbStatus(self, status, tag, box):
1305
line = ' '.join(['%s %s' % x for x in status.iteritems()])
1306
self.sendUntaggedResponse('STATUS %s (%s)' % (box, line))
1307
self.sendPositiveResponse(tag, 'STATUS complete')
1309
def __ebStatus(self, failure, tag, box):
1310
self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, str(failure.value)))
1312
def do_APPEND(self, tag, mailbox, flags, date, message):
1313
mailbox = self._parseMbox(mailbox)
1314
maybeDeferred(self.account.select, mailbox
1315
).addCallback(self._cbAppendGotMailbox, tag, flags, date, message
1316
).addErrback(self._ebAppendGotMailbox, tag
1319
def _cbAppendGotMailbox(self, mbox, tag, flags, date, message):
1321
self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox')
1324
d = mbox.addMessage(message, flags, date)
1325
d.addCallback(self.__cbAppend, tag, mbox)
1326
d.addErrback(self.__ebAppend, tag)
1328
def _ebAppendGotMailbox(self, failure, tag):
1329
self.sendBadResponse(tag, "Server error encountered while opening mailbox.")
1332
auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime,
1334
select_APPEND = auth_APPEND
1336
def __cbAppend(self, result, tag, mbox):
1337
self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount())
1338
self.sendPositiveResponse(tag, 'APPEND complete')
1340
def __ebAppend(self, failure, tag):
1341
self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value))
1343
def do_CHECK(self, tag):
1344
d = self.checkpoint()
1346
self.__cbCheck(None, tag)
1351
callbackArgs=(tag,),
1354
select_CHECK = (do_CHECK,)
1356
def __cbCheck(self, result, tag):
1357
self.sendPositiveResponse(tag, 'CHECK completed')
1359
def __ebCheck(self, failure, tag):
1360
self.sendBadResponse(tag, 'CHECK failed: ' + str(failure.value))
1362
def checkpoint(self):
1363
"""Called when the client issues a CHECK command.
1365
This should perform any checkpoint operations required by the server.
1366
It may be a long running operation, but may not block. If it returns
1367
a deferred, the client will only be informed of success (or failure)
1368
when the deferred's callback (or errback) is invoked.
1372
def do_CLOSE(self, tag):
1374
if self.mbox.isWriteable():
1375
d = maybeDeferred(self.mbox.expunge)
1376
cmbx = ICloseableMailbox(self.mbox, default=None)
1377
if cmbx is not None:
1379
d.addCallback(lambda result: cmbx.close())
1381
d = maybeDeferred(cmbx.close)
1383
d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,), None)
1385
self.__cbClose(None, tag)
1387
select_CLOSE = (do_CLOSE,)
1389
def __cbClose(self, result, tag):
1390
self.sendPositiveResponse(tag, 'CLOSE completed')
1391
self.mbox.removeListener(self)
1395
def __ebClose(self, failure, tag):
1396
self.sendBadResponse(tag, 'CLOSE failed: ' + str(failure.value))
1398
def do_EXPUNGE(self, tag):
1399
if self.mbox.isWriteable():
1400
maybeDeferred(self.mbox.expunge).addCallbacks(
1401
self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None
1404
self.sendNegativeResponse(tag, 'EXPUNGE ignored on read-only mailbox')
1406
select_EXPUNGE = (do_EXPUNGE,)
1408
def __cbExpunge(self, result, tag):
1410
self.sendUntaggedResponse('%d EXPUNGE' % e)
1411
self.sendPositiveResponse(tag, 'EXPUNGE completed')
1413
def __ebExpunge(self, failure, tag):
1414
self.sendBadResponse(tag, 'EXPUNGE failed: ' + str(failure.value))
1417
def do_SEARCH(self, tag, charset, query, uid=0):
1418
sm = ISearchableMailbox(self.mbox, default=None)
1420
maybeDeferred(sm.search, query, uid=uid).addCallbacks(
1421
self.__cbSearch, self.__ebSearch,
1422
(tag, self.mbox, uid), None, (tag,), None
1425
s = parseIdList('1:*')
1426
maybeDeferred(self.mbox.fetch, s, uid=uid).addCallbacks(
1427
self.__cbManualSearch, self.__ebSearch,
1428
(tag, self.mbox, query, uid), None, (tag,), None
1431
select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys)
1433
def __cbSearch(self, result, tag, mbox, uid):
1435
result = map(mbox.getUID, result)
1436
ids = ' '.join([str(i) for i in result])
1437
self.sendUntaggedResponse('SEARCH ' + ids)
1438
self.sendPositiveResponse(tag, 'SEARCH completed')
1440
def __cbManualSearch(self, result, tag, mbox, query, uid, searchResults = None):
1441
if searchResults is None:
1444
for (i, (id, msg)) in zip(range(5), result):
1445
if self.searchFilter(query, id, msg):
1447
searchResults.append(str(msg.getUID()))
1449
searchResults.append(str(id))
1451
from twisted.internet import reactor
1452
reactor.callLater(0, self.__cbManualSearch, result, tag, mbox, query, uid, searchResults)
1455
self.sendUntaggedResponse('SEARCH ' + ' '.join(searchResults))
1456
self.sendPositiveResponse(tag, 'SEARCH completed')
1458
def searchFilter(self, query, id, msg):
1460
if not self.singleSearchStep(query, id, msg):
1464
def singleSearchStep(self, query, id, msg):
1466
if isinstance(q, list):
1467
if not self.searchFilter(q, id, msg):
1471
f = getattr(self, 'search_' + c)
1473
if not f(query, id, msg):
1476
# IMAP goes *out of its way* to be complex
1477
# Sequence sets to search should be specified
1478
# with a command, like EVERYTHING ELSE.
1482
log.err('Unknown search term: ' + c)
1488
def search_ALL(self, query, id, msg):
1491
def search_ANSWERED(self, query, id, msg):
1492
return '\\Answered' in msg.getFlags()
1494
def search_BCC(self, query, id, msg):
1495
bcc = msg.getHeaders(False, 'bcc').get('bcc', '')
1496
return bcc.lower().find(query.pop(0).lower()) != -1
1498
def search_BEFORE(self, query, id, msg):
1499
date = parseTime(query.pop(0))
1500
return rfc822.parsedate(msg.getInternalDate()) < date
1502
def search_BODY(self, query, id, msg):
1503
body = query.pop(0).lower()
1504
return text.strFile(body, msg.getBodyFile(), False)
1506
def search_CC(self, query, id, msg):
1507
cc = msg.getHeaders(False, 'cc').get('cc', '')
1508
return cc.lower().find(query.pop(0).lower()) != -1
1510
def search_DELETED(self, query, id, msg):
1511
return '\\Deleted' in msg.getFlags()
1513
def search_DRAFT(self, query, id, msg):
1514
return '\\Draft' in msg.getFlags()
1516
def search_FLAGGED(self, query, id, msg):
1517
return '\\Flagged' in msg.getFlags()
1519
def search_FROM(self, query, id, msg):
1520
fm = msg.getHeaders(False, 'from').get('from', '')
1521
return fm.lower().find(query.pop(0).lower()) != -1
1523
def search_HEADER(self, query, id, msg):
1524
hdr = query.pop(0).lower()
1525
hdr = msg.getHeaders(False, hdr).get(hdr, '')
1526
return hdr.lower().find(query.pop(0).lower()) != -1
1528
def search_KEYWORD(self, query, id, msg):
1532
def search_LARGER(self, query, id, msg):
1533
return int(query.pop(0)) < msg.getSize()
1535
def search_NEW(self, query, id, msg):
1536
return '\\Recent' in msg.getFlags() and '\\Seen' not in msg.getFlags()
1538
def search_NOT(self, query, id, msg):
1539
return not self.singleSearchStep(query, id, msg)
1541
def search_OLD(self, query, id, msg):
1542
return '\\Recent' not in msg.getFlags()
1544
def search_ON(self, query, id, msg):
1545
date = parseTime(query.pop(0))
1546
return rfc822.parsedate(msg.getInternalDate()) == date
1548
def search_OR(self, query, id, msg):
1549
a = self.singleSearchStep(query, id, msg)
1550
b = self.singleSearchStep(query, id, msg)
1553
def search_RECENT(self, query, id, msg):
1554
return '\\Recent' in msg.getFlags()
1556
def search_SEEN(self, query, id, msg):
1557
return '\\Seen' in msg.getFlags()
1559
def search_SENTBEFORE(self, query, id, msg):
1560
date = msg.getHeader(False, 'date').get('date', '')
1561
date = rfc822.parsedate(date)
1562
return date < parseTime(query.pop(0))
1564
def search_SENTON(self, query, id, msg):
1565
date = msg.getHeader(False, 'date').get('date', '')
1566
date = rfc822.parsedate(date)
1567
return date[:3] == parseTime(query.pop(0))[:3]
1569
def search_SENTSINCE(self, query, id, msg):
1570
date = msg.getHeader(False, 'date').get('date', '')
1571
date = rfc822.parsedate(date)
1572
return date > parseTime(query.pop(0))
1574
def search_SINCE(self, query, id, msg):
1575
date = parseTime(query.pop(0))
1576
return rfc822.parsedate(msg.getInternalDate()) > date
1578
def search_SMALLER(self, query, id, msg):
1579
return int(query.pop(0)) > msg.getSize()
1581
def search_SUBJECT(self, query, id, msg):
1582
subj = msg.getHeaders(False, 'subject').get('subject', '')
1583
return subj.lower().find(query.pop(0).lower()) != -1
1585
def search_TEXT(self, query, id, msg):
1586
# XXX - This must search headers too
1587
body = query.pop(0).lower()
1588
return text.strFile(body, msg.getBodyFile(), False)
1590
def search_TO(self, query, id, msg):
1591
to = msg.getHeaders(False, 'to').get('to', '')
1592
return to.lower().find(query.pop(0).lower()) != -1
1594
def search_UID(self, query, id, msg):
1597
return msg.getUID() in m
1599
def search_UNANSWERED(self, query, id, msg):
1600
return '\\Answered' not in msg.getFlags()
1602
def search_UNDELETED(self, query, id, msg):
1603
return '\\Deleted' not in msg.getFlags()
1605
def search_UNDRAFT(self, query, id, msg):
1606
return '\\Draft' not in msg.getFlags()
1608
def search_UNFLAGGED(self, query, id, msg):
1609
return '\\Flagged' not in msg.getFlags()
1611
def search_UNKEYWORD(self, query, id, msg):
1615
def search_UNSEEN(self, query, id, msg):
1616
return '\\Seen' not in msg.getFlags()
1618
def __ebSearch(self, failure, tag):
1619
self.sendBadResponse(tag, 'SEARCH failed: ' + str(failure.value))
1622
def do_FETCH(self, tag, messages, query, uid=0):
1624
maybeDeferred(self.mbox.fetch, messages, uid=uid).addCallbacks(
1625
self.__cbFetch, self.__ebFetch,
1626
(tag, query, uid), None, (tag,), None
1629
self.sendPositiveResponse(tag, 'FETCH complete')
1631
select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt)
1633
def __cbFetch(self, results, tag, query, uid):
1634
if self.blocked is None:
1637
id, msg = results.next()
1638
except StopIteration:
1639
self.sendPositiveResponse(tag, 'FETCH completed')
1642
self.spewMessage(id, msg, query, uid
1643
).addCallback(lambda _: self.__cbFetch(results, tag, query, uid)
1644
).addErrback(self.__ebSpewMessage
1647
def __ebSpewMessage(self, failure):
1648
# This indicates a programming error.
1649
# There's no reliable way to indicate anything to the client, since we
1650
# may have already written an arbitrary amount of data in response to
1653
self.transport.loseConnection()
1655
def spew_envelope(self, id, msg, _w=None, _f=None):
1657
_w = self.transport.write
1658
_w('ENVELOPE ' + collapseNestedLists([getEnvelope(msg)]))
1660
def spew_flags(self, id, msg, _w=None, _f=None):
1662
_w = self.transport.write
1663
_w('FLAGS ' + '(%s)' % (' '.join(msg.getFlags())))
1665
def spew_internaldate(self, id, msg, _w=None, _f=None):
1667
_w = self.transport.write
1668
idate = msg.getInternalDate()
1669
ttup = rfc822.parsedate_tz(idate)
1671
log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate))
1672
raise IMAP4Exception("Internal failure generating INTERNALDATE")
1674
odate = time.strftime("%d-%b-%Y %H:%M:%S ", ttup[:9])
1676
odate = odate + "+0000"
1682
odate = odate + sign + string.zfill(str(((abs(ttup[9]) / 3600) * 100 + (abs(ttup[9]) % 3600) / 60)), 4)
1683
_w('INTERNALDATE ' + _quote(odate))
1685
def spew_rfc822header(self, id, msg, _w=None, _f=None):
1687
_w = self.transport.write
1688
hdrs = _formatHeaders(msg.getHeaders(True))
1689
_w('RFC822.HEADER ' + _literal(hdrs))
1691
def spew_rfc822text(self, id, msg, _w=None, _f=None):
1693
_w = self.transport.write
1696
return FileProducer(msg.getBodyFile()
1697
).beginProducing(self.transport
1700
def spew_rfc822size(self, id, msg, _w=None, _f=None):
1702
_w = self.transport.write
1703
_w('RFC822.SIZE ' + str(msg.getSize()))
1705
def spew_rfc822(self, id, msg, _w=None, _f=None):
1707
_w = self.transport.write
1710
mf = IMessageFile(msg, default=None)
1712
return FileProducer(mf.open()
1713
).beginProducing(self.transport
1715
return MessageProducer(msg
1716
).beginProducing(self.transport
1719
def spew_uid(self, id, msg, _w=None, _f=None):
1721
_w = self.transport.write
1722
_w('UID ' + str(msg.getUID()))
1724
def spew_bodystructure(self, id, msg, _w=None, _f=None):
1725
_w('BODYSTRUCTURE ' + collapseNestedLists([getBodyStructure(msg, True)]))
1727
def spew_body(self, part, id, msg, _w=None, _f=None):
1729
_w = self.transport.write
1730
for p in part.part or ():
1731
msg = msg.getSubPart(p)
1733
hdrs = msg.getHeaders(part.header.negate, *part.header.fields)
1734
hdrs = _formatHeaders(hdrs)
1735
_w(str(part) + ' ' + _literal(hdrs))
1739
return FileProducer(msg.getBodyFile()
1740
).beginProducing(self.transport
1743
hdrs = _formatHeaders(msg.getHeaders(True))
1744
_w(str(part) + ' ' + _literal(hdrs))
1748
mf = IMessageFile(msg, default=None)
1750
return FileProducer(mf.open()).beginProducing(self.transport)
1751
return MessageProducer(msg).beginProducing(self.transport)
1753
_w('BODY ' + collapseNestedLists([getBodyStructure(msg)]))
1755
def spewMessage(self, id, msg, query, uid):
1756
wbuf = WriteBuffer(self.transport)
1760
write('* %d FETCH (' % (id,))
1770
if part.type == 'uid':
1772
if part.type == 'body':
1773
yield self.spew_body(part, id, msg, write, flush)
1775
f = getattr(self, 'spew_' + part.type)
1776
yield f(id, msg, write, flush)
1777
if part is not query[-1]:
1779
if uid and not seenUID:
1781
yield self.spew_uid(id, msg, write, flush)
1784
return iterateInReactor(spew())
1786
def __ebFetch(self, failure, tag):
1788
self.sendBadResponse(tag, 'FETCH failed: ' + str(failure.value))
1790
def do_STORE(self, tag, messages, mode, flags, uid=0):
1792
silent = mode.endswith('SILENT')
1793
if mode.startswith('+'):
1795
elif mode.startswith('-'):
1800
maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallbacks(
1801
self.__cbStore, self.__ebStore, (tag, self.mbox, uid, silent), None, (tag,), None
1804
select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist)
1806
def __cbStore(self, result, tag, mbox, uid, silent):
1807
if result and not silent:
1808
for (k, v) in result.iteritems():
1810
uidstr = ' UID %d' % mbox.getUID(k)
1813
self.sendUntaggedResponse('%d FETCH (FLAGS (%s)%s)' %
1814
(k, ' '.join(v), uidstr))
1815
self.sendPositiveResponse(tag, 'STORE completed')
1817
def __ebStore(self, failure, tag):
1818
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1820
def do_COPY(self, tag, messages, mailbox, uid=0):
1821
mailbox = self._parseMbox(mailbox)
1822
maybeDeferred(self.account.select, mailbox
1823
).addCallback(self._cbCopySelectedMailbox, tag, messages, mailbox, uid
1824
).addErrback(self._ebCopySelectedMailbox, tag
1826
select_COPY = (do_COPY, arg_seqset, arg_astring)
1828
def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid):
1830
self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox)
1832
maybeDeferred(self.mbox.fetch, messages, uid
1833
).addCallback(self.__cbCopy, tag, mbox
1834
).addCallback(self.__cbCopied, tag, mbox
1835
).addErrback(self.__ebCopy, tag
1838
def _ebCopySelectedMailbox(self, failure, tag):
1839
self.sendBadResponse(tag, 'Server error: ' + str(failure.value))
1841
def __cbCopy(self, messages, tag, mbox):
1842
# XXX - This should handle failures with a rollback or something
1847
fastCopyMbox = IMessageCopier(mbox, default=None)
1848
for (id, msg) in messages:
1849
if fastCopyMbox is not None:
1850
d = maybeDeferred(fastCopyMbox.copy, msg)
1851
addedDeferreds.append(d)
1854
# XXX - The following should be an implementation of IMessageCopier.copy
1855
# on an IMailbox->IMessageCopier adapter.
1857
flags = msg.getFlags()
1858
date = msg.getInternalDate()
1860
body = IMessageFile(msg, default=None)
1861
if body is not None:
1862
bodyFile = body.open()
1863
d = maybeDeferred(mbox.addMessage, bodyFile, flags, date)
1868
buffer = tempfile.TemporaryFile()
1869
d = MessageProducer(msg, buffer
1870
).beginProducing(None
1871
).addCallback(lambda _, b=buffer, f=flags, d=date: mbox.addMessage(rewind(b), f, d)
1873
addedDeferreds.append(d)
1874
return defer.DeferredList(addedDeferreds)
1876
def __cbCopied(self, deferredIds, tag, mbox):
1879
for (status, result) in deferredIds:
1883
failures.append(result.value)
1885
self.sendNegativeResponse(tag, '[ALERT] Some messages were not copied')
1887
self.sendPositiveResponse(tag, 'COPY completed')
1889
def __ebCopy(self, failure, tag):
1890
self.sendBadResponse(tag, 'COPY failed:' + str(failure.value))
1893
def do_UID(self, tag, command, line):
1894
command = command.upper()
1896
if command not in ('COPY', 'FETCH', 'STORE', 'SEARCH'):
1897
raise IllegalClientResponse(command)
1899
self.dispatchCommand(tag, command, line, uid=1)
1901
select_UID = (do_UID, arg_atom, arg_line)
1903
# IMailboxListener implementation
1905
def modeChanged(self, writeable):
1907
self.sendUntaggedResponse(message='[READ-WRITE]', async=True)
1909
self.sendUntaggedResponse(message='[READ-ONLY]', async=True)
1911
def flagsChanged(self, newFlags):
1912
for (mId, flags) in newFlags.iteritems():
1913
msg = '%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags))
1914
self.sendUntaggedResponse(msg, async=True)
1916
def newMessages(self, exists, recent):
1917
if exists is not None:
1918
self.sendUntaggedResponse('%d EXISTS' % exists, async=True)
1919
if recent is not None:
1920
self.sendUntaggedResponse('%d RECENT' % recent, async=True)
1922
class UnhandledResponse(IMAP4Exception): pass
1924
class NegativeResponse(IMAP4Exception): pass
1926
class NoSupportedAuthentication(IMAP4Exception):
1927
def __init__(self, serverSupports, clientSupports):
1928
IMAP4Exception.__init__(self, 'No supported authentication schemes available')
1929
self.serverSupports = serverSupports
1930
self.clientSupports = clientSupports
1933
return (IMAP4Exception.__str__(self)
1934
+ ': Server supports %r, client supports %r'
1935
% (self.serverSupports, self.clientSupports))
1937
class IllegalServerResponse(IMAP4Exception): pass
1939
class IMAP4Client(basic.LineReceiver):
1940
"""IMAP4 client protocol implementation
1942
@ivar state: A string representing the state the connection is currently
1945
__implements__ = (IMailboxListener,)
1955
# Capabilities are not allowed to change during the session
1956
# So cache the first response and use that for all later
1960
_memoryFileLimit = 1024 * 1024 * 10
1962
# Authentication is pluggable. This maps names to IClientAuthentication
1964
authenticators = None
1966
STATUS_CODES = ('OK', 'NO', 'BAD', 'PREAUTH', 'BYE')
1968
STATUS_TRANSFORMATIONS = {
1969
'MESSAGES': int, 'RECENT': int, 'UNSEEN': int
1974
def __init__(self, contextFactory = None):
1977
self.authenticators = {}
1978
self.context = contextFactory
1983
def registerAuthenticator(self, auth):
1984
"""Register a new form of authentication
1986
When invoking the authenticate() method of IMAP4Client, the first
1987
matching authentication scheme found will be used. The ordering is
1988
that in which the server lists support authentication schemes.
1990
@type auth: Implementor of C{IClientAuthentication}
1991
@param auth: The object to use to perform the client
1992
side of this authentication scheme.
1994
self.authenticators[auth.getName().upper()] = auth
1996
def rawDataReceived(self, data):
1997
self._pendingSize -= len(data)
1998
if self._pendingSize > 0:
1999
self._pendingBuffer.write(data)
2002
if self._pendingSize < 0:
2003
data, passon = data[:self._pendingSize], data[self._pendingSize:]
2004
self._pendingBuffer.write(data)
2005
rest = self._pendingBuffer
2006
self._pendingBuffer = None
2007
self._pendingSize = None
2009
self._parts.append(rest.read())
2010
self.setLineMode(passon.lstrip('\r\n'))
2012
# def sendLine(self, line):
2013
# print 'S:', repr(line)
2014
# return basic.LineReceiver.sendLine(self, line)
2016
def _setupForLiteral(self, rest, octets):
2017
self._pendingBuffer = self.messageFile(octets)
2018
self._pendingSize = octets
2019
self._parts = [rest, '\r\n']
2022
def lineReceived(self, line):
2023
# print 'C: ' + repr(line)
2024
if self._parts is None:
2025
lastPart = line.rfind(' ')
2027
lastPart = line[lastPart + 1:]
2028
if lastPart.startswith('{') and lastPart.endswith('}'):
2029
# It's a literal a-comin' in
2031
octets = int(lastPart[1:-1])
2033
raise IllegalServerResponse(line)
2034
self._tag, parts = line.split(None, 1)
2035
self._setupForLiteral(parts, octets)
2038
# It isn't a literal at all
2039
self._regularDispatch(line)
2041
self._regularDispatch(line)
2043
# If an expression is in progress, no tag is required here
2044
# Since we didn't find a literal indicator, this expression
2046
self._parts.append(line)
2047
tag, rest = self._tag, ''.join(self._parts)
2048
self._tag = self._parts = None
2049
self.dispatchCommand(tag, rest)
2051
def _regularDispatch(self, line):
2052
parts = line.split(None, 1)
2056
self.dispatchCommand(tag, rest)
2058
def messageFile(self, octets):
2059
"""Create a file to which an incoming message may be written.
2061
@type octets: C{int}
2062
@param octets: The number of octets which will be written to the file
2064
@rtype: Any object which implements C{write(string)} and
2066
@return: A file-like object
2068
if octets > self._memoryFileLimit:
2069
return tempfile.TemporaryFile()
2071
return StringIO.StringIO()
2074
tag = '%0.4X' % self.tagID
2078
def dispatchCommand(self, tag, rest):
2079
if self.state is None:
2080
f = self.response_UNAUTH
2082
f = getattr(self, 'response_' + self.state.upper(), None)
2088
self.transport.loseConnection()
2090
log.err("Cannot dispatch: %s, %s, %s" % (self.state, tag, rest))
2091
self.transport.loseConnection()
2093
def response_UNAUTH(self, tag, rest):
2094
if self.state is None:
2095
# Server greeting, this is
2096
status, rest = rest.split(None, 1)
2097
if status.upper() == 'OK':
2098
self.state = 'unauth'
2099
elif status.upper() == 'PREAUTH':
2102
# XXX - This is rude.
2103
self.transport.loseConnection()
2104
raise IllegalServerResponse(tag + ' ' + rest)
2106
b, e = rest.find('['), rest.find(']')
2107
if b != -1 and e != -1:
2108
self.serverGreeting(self.__cbCapabilities(([rest[b:e]], None)))
2110
self.serverGreeting(None)
2112
self._defaultHandler(tag, rest)
2114
def response_AUTH(self, tag, rest):
2115
self._defaultHandler(tag, rest)
2117
def _defaultHandler(self, tag, rest):
2118
if tag == '*' or tag == '+':
2119
if not self.waiting:
2120
self._extraInfo([rest])
2122
cmd = self.tags[self.waiting]
2124
cmd.continuation(rest)
2126
cmd.lines.append(rest)
2129
cmd = self.tags[tag]
2131
# XXX - This is rude.
2132
self.transport.loseConnection()
2133
raise IllegalServerResponse(tag + ' ' + rest)
2135
status, line = rest.split(None, 1)
2137
# Give them this last line, too
2138
cmd.finish(rest, self._extraInfo)
2140
cmd.defer.errback(IMAP4Exception(line))
2145
def _flushQueue(self):
2147
cmd = self.queued.pop(0)
2150
self.sendLine(cmd.format(t))
2153
def _extraInfo(self, lines):
2154
# XXX - This is terrible.
2155
# XXX - Also, this should collapse temporally proximate calls into single
2156
# invocations of IMailboxListener methods, where possible.
2158
recent = exists = None
2160
if L.find('EXISTS') != -1:
2161
exists = int(L.split()[0])
2162
elif L.find('RECENT') != -1:
2163
recent = int(L.split()[0])
2164
elif L.find('READ-ONLY') != -1:
2166
elif L.find('READ-WRITE') != -1:
2168
elif L.find('FETCH') != -1:
2169
for (mId, fetched) in self.__cbFetch(([L], None)).iteritems():
2171
for f in fetched.get('FLAGS', []):
2173
flags.setdefault(mId, []).extend(sum)
2175
log.msg('Unhandled unsolicited response: ' + repr(L))
2177
self.flagsChanged(flags)
2178
if recent is not None or exists is not None:
2179
self.newMessages(exists, recent)
2181
def sendCommand(self, cmd):
2182
cmd.defer = defer.Deferred()
2184
self.queued.append(cmd)
2188
self.sendLine(cmd.format(t))
2192
def getCapabilities(self, useCache=1):
2193
"""Request the capabilities available on this server.
2195
This command is allowed in any state of connection.
2197
@type useCache: C{bool}
2198
@param useCache: Specify whether to use the capability-cache or to
2199
re-retrieve the capabilities from the server. Server capabilities
2200
should never change, so for normal use, this flag should never be
2204
@return: A deferred whose callback will be invoked with a
2205
dictionary mapping capability types to lists of supported
2206
mechanisms, or to None if a support list is not applicable.
2208
if useCache and self._capCache is not None:
2209
return defer.succeed(self._capCache)
2211
resp = ('CAPABILITY',)
2212
d = self.sendCommand(Command(cmd, wantResponse=resp))
2213
d.addCallback(self.__cbCapabilities)
2216
def __cbCapabilities(self, (lines, tagline)):
2219
rest = rest.split()[1:]
2225
caps.setdefault(cap[:eq], []).append(cap[eq+1:])
2226
self._capCache = caps
2230
"""Inform the server that we are done with the connection.
2232
This command is allowed in any state of connection.
2235
@return: A deferred whose callback will be invoked with None
2236
when the proper server acknowledgement has been received.
2238
d = self.sendCommand(Command('LOGOUT', wantResponse=('BYE',)))
2239
d.addCallback(self.__cbLogout)
2242
def __cbLogout(self, (lines, tagline)):
2243
self.transport.loseConnection()
2244
# We don't particularly care what the server said
2249
"""Perform no operation.
2251
This command is allowed in any state of connection.
2254
@return: A deferred whose callback will be invoked with a list
2255
of untagged status updates the server responds with.
2257
d = self.sendCommand(Command('NOOP'))
2258
d.addCallback(self.__cbNoop)
2261
def __cbNoop(self, (lines, tagline)):
2262
# Conceivable, this is elidable.
2263
# It is, afterall, a no-op.
2267
def authenticate(self, secret):
2268
"""Attempt to enter the authenticated state with the server
2270
This command is allowed in the Non-Authenticated state.
2273
@return: A deferred whose callback is invoked if the authentication
2274
succeeds and whose errback will be invoked otherwise.
2276
if self._capCache is None:
2277
d = self.getCapabilities()
2279
d = defer.succeed(self._capCache)
2280
d.addCallback(self.__cbAuthenticate, secret)
2283
def __cbAuthenticate(self, caps, secret):
2284
auths = caps.get('AUTH', ())
2285
for scheme in auths:
2286
if scheme.upper() in self.authenticators:
2289
if 'STARTTLS' in caps:
2290
tls = interfaces.ITLSTransport(self.transport, default=None)
2292
ctx = self._getContextFactory()
2294
d = self.sendCommand(Command('STARTTLS'))
2295
d.addCallback(self._startedTLS, ctx)
2296
d.addCallback(lambda _: self.getCapabilities())
2297
d.addCallback(self.__cbAuthTLS, secret)
2299
raise NoSupportedAuthentication(auths, self.authenticators.keys())
2301
d = self.sendCommand(Command('AUTHENTICATE', scheme, (), self.__cbContinueAuth, scheme, secret))
2304
def __cbContinueAuth(self, rest, scheme, secret):
2306
chal = base64.decodestring(rest + '\n')
2307
except binascii.Error:
2309
raise IllegalServerResponse(rest)
2310
self.transport.loseConnection()
2312
auth = self.authenticators[scheme]
2313
chal = auth.challengeResponse(secret, chal)
2314
self.sendLine(base64.encodestring(chal).strip())
2316
def __cbAuthTLS(self, caps, secret):
2317
auths = caps.get('AUTH', ())
2318
for scheme in auths:
2319
if scheme.upper() in self.authenticators:
2322
raise NoSupportedAuthentication(auths, self.authenticators.keys())
2324
d = self.sendCommand(Command('AUTHENTICATE', scheme, (), self.__cbContinueAuth, scheme, secret))
2327
def login(self, username, password):
2328
"""Authenticate with the server using a username and password
2330
This command is allowed in the Non-Authenticated state. If the
2331
server supports the STARTTLS capability and our transport supports
2332
TLS, TLS is negotiated before the login command is issued.
2334
@type username: C{str}
2335
@param username: The username to log in with
2337
@type password: C{str}
2338
@param password: The password to log in with
2341
@return: A deferred whose callback is invoked if login is successful
2342
and whose errback is invoked otherwise.
2344
d = maybeDeferred(self.getCapabilities)
2348
callbackArgs=(username, password),
2352
def serverGreeting(self, caps):
2353
"""Called when the server has sent us a greeting.
2356
@param caps: Capabilities the server advertised in its greeting.
2359
def _getContextFactory(self):
2360
if self.context is not None:
2363
from twisted.internet import ssl
2367
context = ssl.ClientContextFactory()
2368
context.method = ssl.SSL.TLSv1_METHOD
2371
def __cbLoginCaps(self, capabilities, username, password):
2372
tryTLS = 'STARTTLS' in capabilities and (interfaces.ITLSTransport(self.transport, default=None) is not None)
2374
ctx = self._getContextFactory()
2376
d = self.sendCommand(Command('STARTTLS'))
2377
d.addCallback(self._startedTLS, ctx)
2381
callbackArgs=(username, password),
2385
log.err("Server wants us to use TLS, but we don't have "
2386
"a Context Factory!")
2388
if self.transport.getHost()[0] != 'SSL':
2389
log.msg("Server has no TLS support. logging in over cleartext!")
2390
args = ' '.join((username, password))
2391
return self.sendCommand(Command('LOGIN', args))
2393
def _startedTLS(self, result, context):
2394
self.context = context
2395
self.transport.startTLS(context)
2396
self._capCache = None
2397
self.startedTLS = True
2400
def __ebLoginCaps(self, failure):
2404
def __cbLoginTLS(self, result, username, password):
2405
args = ' '.join((username, password))
2406
return self.sendCommand(Command('LOGIN', args))
2408
def __ebLoginTLS(self, failure):
2412
def namespace(self):
2413
"""Retrieve information about the namespaces available to this account
2415
This command is allowed in the Authenticated and Selected states.
2418
@return: A deferred whose callback is invoked with namespace
2419
information. An example of this information is:
2421
[[['', '/']], [], []]
2423
which indicates a single personal namespace called '' with '/'
2424
as its hierarchical delimiter, and no shared or user namespaces.
2427
resp = ('NAMESPACE',)
2428
d = self.sendCommand(Command(cmd, wantResponse=resp))
2429
d.addCallback(self.__cbNamespace)
2432
def __cbNamespace(self, (lines, last)):
2434
parts = line.split(None, 1)
2436
if parts[0] == 'NAMESPACE':
2437
# XXX UGGG parsing hack :(
2438
r = parseNestedParens('(' + parts[1] + ')')[0]
2439
return [e or [] for e in r]
2440
log.err("No NAMESPACE response to NAMESPACE command")
2443
def select(self, mailbox):
2446
This command is allowed in the Authenticated and Selected states.
2448
@type mailbox: C{str}
2449
@param mailbox: The name of the mailbox to select
2452
@return: A deferred whose callback is invoked with mailbox
2453
information if the select is successful and whose errback is
2454
invoked otherwise. Mailbox information consists of a dictionary
2455
with the following keys and values::
2457
FLAGS: A list of strings containing the flags settable on
2458
messages in this mailbox.
2460
EXISTS: An integer indicating the number of messages in this
2463
RECENT: An integer indicating the number of \"recent\"
2464
messages in this mailbox.
2466
UNSEEN: An integer indicating the number of messages not
2467
flagged \\Seen in this mailbox.
2469
PERMANENTFLAGS: A list of strings containing the flags that
2470
can be permanently set on messages in this mailbox.
2472
UIDVALIDITY: An integer uniquely identifying this mailbox.
2475
args = mailbox.encode('imap4-utf-7')
2476
if _needsQuote(args):
2478
resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
2479
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2480
d.addCallback(self.__cbSelect, 1)
2483
def examine(self, mailbox):
2484
"""Select a mailbox in read-only mode
2486
This command is allowed in the Authenticated and Selected states.
2488
@type mailbox: C{str}
2489
@param mailbox: The name of the mailbox to examine
2492
@return: A deferred whose callback is invoked with mailbox
2493
information if the examine is successful and whose errback
2494
is invoked otherwise. Mailbox information consists of a dictionary
2495
with the following keys and values::
2497
'FLAGS': A list of strings containing the flags settable on
2498
messages in this mailbox.
2500
'EXISTS': An integer indicating the number of messages in this
2503
'RECENT': An integer indicating the number of \"recent\"
2504
messages in this mailbox.
2506
'UNSEEN': An integer indicating the number of messages not
2507
flagged \\Seen in this mailbox.
2509
'PERMANENTFLAGS': A list of strings containing the flags that
2510
can be permanently set on messages in this mailbox.
2512
'UIDVALIDITY': An integer uniquely identifying this mailbox.
2515
args = mailbox.encode('imap4-utf-7')
2516
if _needsQuote(args):
2518
resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY')
2519
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2520
d.addCallback(self.__cbSelect, 0)
2523
def __cbSelect(self, (lines, tagline), rw):
2524
# In the absense of specification, we are free to assume:
2526
datum = {'READ-WRITE': rw}
2527
lines.append(tagline)
2529
split = parts.split()
2531
if split[1].upper().strip() == 'EXISTS':
2533
datum['EXISTS'] = int(split[0])
2535
raise IllegalServerResponse(parts)
2536
elif split[1].upper().strip() == 'RECENT':
2538
datum['RECENT'] = int(split[0])
2540
raise IllegalServerResponse(parts)
2542
log.err('Unhandled SELECT response (1): ' + parts)
2543
elif split[0].upper().strip() == 'FLAGS':
2544
split = parts.split(None, 1)
2545
datum['FLAGS'] = tuple(parseNestedParens(split[1])[0])
2546
elif split[0].upper().strip() == 'OK':
2547
begin = parts.find('[')
2548
end = parts.find(']')
2549
if begin == -1 or end == -1:
2550
raise IllegalServerResponse(parts)
2552
content = parts[begin+1:end].split(None, 1)
2553
if len(content) >= 1:
2554
key = content[0].upper()
2555
if key == 'READ-ONLY':
2556
datum['READ-WRITE'] = 0
2557
elif key == 'READ-WRITE':
2558
datum['READ-WRITE'] = 1
2559
elif key == 'UIDVALIDITY':
2561
datum['UIDVALIDITY'] = int(content[1])
2563
raise IllegalServerResponse(parts)
2564
elif key == 'UNSEEN':
2566
datum['UNSEEN'] = int(content[1])
2568
raise IllegalServerResponse(parts)
2569
elif key == 'UIDNEXT':
2570
datum['UIDNEXT'] = int(content[1])
2571
elif key == 'PERMANENTFLAGS':
2572
datum['PERMANENTFLAGS'] = tuple(parseNestedParens(content[1])[0])
2574
log.err('Unhandled SELECT response (2): ' + parts)
2576
log.err('Unhandled SELECT response (3): ' + parts)
2578
log.err('Unhandled SELECT response (4): ' + parts)
2581
def create(self, name):
2582
"""Create a new mailbox on the server
2584
This command is allowed in the Authenticated and Selected states.
2587
@param name: The name of the mailbox to create.
2590
@return: A deferred whose callback is invoked if the mailbox creation
2591
is successful and whose errback is invoked otherwise.
2593
return self.sendCommand(Command('CREATE', name.encode('imap4-utf-7')))
2595
def delete(self, name):
2598
This command is allowed in the Authenticated and Selected states.
2601
@param name: The name of the mailbox to delete.
2604
@return: A deferred whose calblack is invoked if the mailbox is
2605
deleted successfully and whose errback is invoked otherwise.
2607
return self.sendCommand(Command('DELETE', name.encode('imap4-utf-7')))
2609
def rename(self, oldname, newname):
2612
This command is allowed in the Authenticated and Selected states.
2614
@type oldname: C{str}
2615
@param oldname: The current name of the mailbox to rename.
2617
@type newname: C{str}
2618
@param newname: The new name to give the mailbox.
2621
@return: A deferred whose callback is invoked if the rename is
2622
successful and whose errback is invoked otherwise.
2624
oldname = oldname.encode('imap4-utf-7')
2625
newname = newname.encode('imap4-utf-7')
2626
return self.sendCommand(Command('RENAME', ' '.join((oldname, newname))))
2628
def subscribe(self, name):
2629
"""Add a mailbox to the subscription list
2631
This command is allowed in the Authenticated and Selected states.
2634
@param name: The mailbox to mark as 'active' or 'subscribed'
2637
@return: A deferred whose callback is invoked if the subscription
2638
is successful and whose errback is invoked otherwise.
2640
return self.sendCommand(Command('SUBSCRIBE', name.encode('imap4-utf-7')))
2642
def unsubscribe(self, name):
2643
"""Remove a mailbox from the subscription list
2645
This command is allowed in the Authenticated and Selected states.
2648
@param name: The mailbox to unsubscribe
2651
@return: A deferred whose callback is invoked if the unsubscription
2652
is successful and whose errback is invoked otherwise.
2654
return self.sendCommand(Command('UNSUBSCRIBE', name.encode('imap4-utf-7')))
2656
def list(self, reference, wildcard):
2657
"""List a subset of the available mailboxes
2659
This command is allowed in the Authenticated and Selected states.
2661
@type reference: C{str}
2662
@param reference: The context in which to interpret C{wildcard}
2664
@type wildcard: C{str}
2665
@param wildcard: The pattern of mailbox names to match, optionally
2666
including either or both of the '*' and '%' wildcards. '*' will
2667
match zero or more characters and cross hierarchical boundaries.
2668
'%' will also match zero or more characters, but is limited to a
2669
single hierarchical level.
2672
@return: A deferred whose callback is invoked with a list of C{tuple}s,
2673
the first element of which is a C{tuple} of mailbox flags, the second
2674
element of which is the hierarchy delimiter for this mailbox, and the
2675
third of which is the mailbox name; if the command is unsuccessful,
2676
the deferred's errback is invoked instead.
2679
args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
2681
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2682
d.addCallback(self.__cbList, 'LIST')
2685
def lsub(self, reference, wildcard):
2686
"""List a subset of the subscribed available mailboxes
2688
This command is allowed in the Authenticated and Selected states.
2690
The parameters and returned object are the same as for the C{list}
2691
method, with one slight difference: Only mailboxes which have been
2692
subscribed can be included in the resulting list.
2695
args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7'))
2697
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2698
d.addCallback(self.__cbList, 'LSUB')
2701
def __cbList(self, (lines, last), command):
2704
parts = parseNestedParens(L)
2706
raise IllegalServerResponse, L
2707
if parts[0] == command:
2708
parts[1] = tuple(parts[1])
2709
results.append(tuple(parts[1:]))
2712
def status(self, mailbox, *names):
2713
"""Retrieve the status of the given mailbox
2715
This command is allowed in the Authenticated and Selected states.
2717
@type mailbox: C{str}
2718
@param mailbox: The name of the mailbox to query
2721
@param names: The status names to query. These may be any number of:
2722
MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, and UNSEEN.
2725
@return: A deferred whose callback is invoked with the status information
2726
if the command is successful and whose errback is invoked otherwise.
2729
args = "%s (%s)" % (mailbox.encode('imap4-utf-7'), ' '.join(names))
2731
d = self.sendCommand(Command(cmd, args, wantResponse=resp))
2732
d.addCallback(self.__cbStatus)
2735
def __cbStatus(self, (lines, last)):
2738
parts = parseNestedParens(line)
2739
if parts[0] == 'STATUS':
2741
items = [items[i:i+2] for i in range(0, len(items), 2)]
2742
status.update(dict(items))
2743
for k in status.keys():
2744
t = self.STATUS_TRANSFORMATIONS.get(k)
2747
status[k] = t(status[k])
2748
except Exception, e:
2749
raise IllegalServerResponse('(%s %s): %s' % (k, status[k], str(e)))
2752
def append(self, mailbox, message, flags = (), date = None):
2753
"""Add the given message to the currently selected mailbox
2755
This command is allowed in the Authenticated and Selected states.
2757
@type mailbox: C{str}
2758
@param mailbox: The mailbox to which to add this message.
2760
@type message: Any file-like object
2761
@param message: The message to add, in RFC822 format.
2763
@type flags: Any iterable of C{str}
2764
@param flags: The flags to associated with this message.
2767
@param date: The date to associate with this message.
2770
@return: A deferred whose callback is invoked when this command
2771
succeeds or whose errback is invoked if it fails.
2776
fmt = '%s (%s)%s {%d}'
2778
date = ' "%s"' % date
2782
mailbox.encode('imap4-utf-7'), ' '.join(flags),
2785
d = self.sendCommand(Command('APPEND', cmd, (), self.__cbContinueAppend, message))
2788
def __cbContinueAppend(self, lines, message):
2789
s = basic.FileSender()
2790
return s.beginFileTransfer(message, self.transport, None
2791
).addCallback(self.__cbFinishAppend)
2793
def __cbFinishAppend(self, foo):
2797
"""Tell the server to perform a checkpoint
2799
This command is allowed in the Selected state.
2802
@return: A deferred whose callback is invoked when this command
2803
succeeds or whose errback is invoked if it fails.
2805
return self.sendCommand(Command('CHECK'))
2808
"""Return the connection to the Authenticated state.
2810
This command is allowed in the Selected state.
2812
Issuing this command will also remove all messages flagged \\Deleted
2813
from the selected mailbox if it is opened in read-write mode,
2814
otherwise it indicates success by no messages are removed.
2817
@return: A deferred whose callback is invoked when the command
2818
completes successfully or whose errback is invoked if it fails.
2820
return self.sendCommand(Command('CLOSE'))
2823
"""Return the connection to the Authenticate state.
2825
This command is allowed in the Selected state.
2827
Issuing this command will perform the same actions as issuing the
2828
close command, but will also generate an 'expunge' response for
2829
every message deleted.
2832
@return: A deferred whose callback is invoked with a list of the
2833
'expunge' responses when this command is successful or whose errback
2834
is invoked otherwise.
2838
d = self.sendCommand(Command(cmd, wantResponse=resp))
2839
d.addCallback(self.__cbExpunge)
2842
def __cbExpunge(self, (lines, last)):
2845
parts = line.split(None, 1)
2847
if parts[1] == 'EXPUNGE':
2849
ids.append(int(parts[0]))
2851
raise IllegalServerResponse, line
2854
def search(self, *queries, **kwarg):
2855
"""Search messages in the currently selected mailbox
2857
This command is allowed in the Selected state.
2859
Any non-zero number of queries are accepted by this method, as
2860
returned by the C{Query}, C{Or}, and C{Not} functions.
2862
One keyword argument is accepted: if uid is passed in with a non-zero
2863
value, the server is asked to return message UIDs instead of message
2867
@return: A deferred whose callback will be invoked with a list of all
2868
the message sequence numbers return by the search, or whose errback
2869
will be invoked if there is an error.
2871
if kwarg.get('uid'):
2875
args = ' '.join(queries)
2876
d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,)))
2877
d.addCallback(self.__cbSearch)
2880
def __cbSearch(self, (lines, end)):
2883
parts = line.split(None, 1)
2885
if parts[0] == 'SEARCH':
2887
ids.extend(map(int, parts[1].split()))
2889
raise IllegalServerResponse, line
2892
def fetchUID(self, messages, uid=0):
2893
"""Retrieve the unique identifier for one or more messages
2895
This command is allowed in the Selected state.
2897
@type messages: C{MessageSet} or C{str}
2898
@param messages: A message sequence set
2901
@param uid: Indicates whether the message sequence set is of message
2902
numbers or of unique message IDs.
2905
@return: A deferred whose callback is invoked with a dict mapping
2906
message sequence numbers to unique message identifiers, or whose
2907
errback is invoked if there is an error.
2909
d = self._fetch(messages, useUID=uid, uid=1)
2910
d.addCallback(self.__cbFetch)
2913
def fetchFlags(self, messages, uid=0):
2914
"""Retrieve the flags for one or more messages
2916
This command is allowed in the Selected state.
2918
@type messages: C{MessageSet} or C{str}
2919
@param messages: The messages for which to retrieve flags.
2922
@param uid: Indicates whether the message sequence set is of message
2923
numbers or of unique message IDs.
2926
@return: A deferred whose callback is invoked with a dict mapping
2927
message numbers to lists of flags, or whose errback is invoked if
2930
d = self._fetch(str(messages), useUID=uid, flags=1)
2931
d.addCallback(self.__cbFetch)
2934
def fetchInternalDate(self, messages, uid=0):
2935
"""Retrieve the internal date associated with one or more messages
2937
This command is allowed in the Selected state.
2939
@type messages: C{MessageSet} or C{str}
2940
@param messages: The messages for which to retrieve the internal date.
2943
@param uid: Indicates whether the message sequence set is of message
2944
numbers or of unique message IDs.
2947
@return: A deferred whose callback is invoked with a dict mapping
2948
message numbers to date strings, or whose errback is invoked
2949
if there is an error. Date strings take the format of
2950
\"day-month-year time timezone\".
2952
d = self._fetch(str(messages), useUID=uid, internaldate=1)
2953
d.addCallback(self.__cbFetch)
2956
def fetchEnvelope(self, messages, uid=0):
2957
"""Retrieve the envelope data for one or more messages
2959
This command is allowed in the Selected state.
2961
@type messages: C{MessageSet} or C{str}
2962
@param messages: The messages for which to retrieve envelope data.
2965
@param uid: Indicates whether the message sequence set is of message
2966
numbers or of unique message IDs.
2969
@return: A deferred whose callback is invoked with a dict mapping
2970
message numbers to envelope data, or whose errback is invoked
2971
if there is an error. Envelope data consists of a sequence of the
2972
date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
2973
and message-id header fields. The date, subject, in-reply-to, and
2974
message-id fields are strings, while the from, sender, reply-to,
2975
to, cc, and bcc fields contain address data. Address data consists
2976
of a sequence of name, source route, mailbox name, and hostname.
2977
Fields which are not present for a particular address may be C{None}.
2979
d = self._fetch(str(messages), useUID=uid, envelope=1)
2980
d.addCallback(self.__cbFetch)
2983
def fetchBodyStructure(self, messages, uid=0):
2984
"""Retrieve the structure of the body of one or more messages
2986
This command is allowed in the Selected state.
2988
@type messages: C{MessageSet} or C{str}
2989
@param messages: The messages for which to retrieve body structure
2993
@param uid: Indicates whether the message sequence set is of message
2994
numbers or of unique message IDs.
2997
@return: A deferred whose callback is invoked with a dict mapping
2998
message numbers to body structure data, or whose errback is invoked
2999
if there is an error. Body structure data describes the MIME-IMB
3000
format of a message and consists of a sequence of mime type, mime
3001
subtype, parameters, content id, description, encoding, and size.
3002
The fields following the size field are variable: if the mime
3003
type/subtype is message/rfc822, the contained message's envelope
3004
information, body structure data, and number of lines of text; if
3005
the mime type is text, the number of lines of text. Extension fields
3006
may also be included; if present, they are: the MD5 hash of the body,
3007
body disposition, body language.
3009
d = self._fetch(messages, useUID=uid, bodystructure=1)
3010
d.addCallback(self.__cbFetch)
3013
def fetchSimplifiedBody(self, messages, uid=0):
3014
"""Retrieve the simplified body structure of one or more messages
3016
This command is allowed in the Selected state.
3018
@type messages: C{MessageSet} or C{str}
3019
@param messages: A message sequence set
3022
@param uid: Indicates whether the message sequence set is of message
3023
numbers or of unique message IDs.
3026
@return: A deferred whose callback is invoked with a dict mapping
3027
message numbers to body data, or whose errback is invoked
3028
if there is an error. The simplified body structure is the same
3029
as the body structure, except that extension fields will never be
3032
d = self._fetch(messages, useUID=uid, body=1)
3033
d.addCallback(self.__cbFetch)
3036
def fetchMessage(self, messages, uid=0):
3037
"""Retrieve one or more entire messages
3039
This command is allowed in the Selected state.
3041
@type messages: C{MessageSet} or C{str}
3042
@param messages: A message sequence set
3045
@param uid: Indicates whether the message sequence set is of message
3046
numbers or of unique message IDs.
3049
@return: A deferred whose callback is invoked with a dict mapping
3050
message objects (as returned by self.messageFile(), file objects by
3051
default), or whose errback is invoked if there is an error.
3053
d = self._fetch(messages, useUID=uid, rfc822=1)
3054
d.addCallback(self.__cbFetch)
3057
def fetchHeaders(self, messages, uid=0):
3058
"""Retrieve headers of one or more messages
3060
This command is allowed in the Selected state.
3062
@type messages: C{MessageSet} or C{str}
3063
@param messages: A message sequence set
3066
@param uid: Indicates whether the message sequence set is of message
3067
numbers or of unique message IDs.
3070
@return: A deferred whose callback is invoked with a dict mapping
3071
message numbers to dicts of message headers, or whose errback is
3072
invoked if there is an error.
3074
d = self._fetch(messages, useUID=uid, rfc822header=1)
3075
d.addCallback(self.__cbFetch)
3078
def fetchBody(self, messages, uid=0):
3079
"""Retrieve body text of one or more messages
3081
This command is allowed in the Selected state.
3083
@type messages: C{MessageSet} or C{str}
3084
@param messages: A message sequence set
3087
@param uid: Indicates whether the message sequence set is of message
3088
numbers or of unique message IDs.
3091
@return: A deferred whose callback is invoked with a dict mapping
3092
message numbers to file-like objects containing body text, or whose
3093
errback is invoked if there is an error.
3095
d = self._fetch(messages, useUID=uid, rfc822text=1)
3096
d.addCallback(self.__cbFetch)
3099
def fetchSize(self, messages, uid=0):
3100
"""Retrieve the size, in octets, of one or more messages
3102
This command is allowed in the Selected state.
3104
@type messages: C{MessageSet} or C{str}
3105
@param messages: A message sequence set
3108
@param uid: Indicates whether the message sequence set is of message
3109
numbers or of unique message IDs.
3112
@return: A deferred whose callback is invoked with a dict mapping
3113
message numbers to sizes, or whose errback is invoked if there is
3116
d = self._fetch(messages, useUID=uid, rfc822size=1)
3117
d.addCallback(self.__cbFetch)
3120
def fetchFull(self, messages, uid=0):
3121
"""Retrieve several different fields of one or more messages
3123
This command is allowed in the Selected state. This is equivalent
3124
to issuing all of the C{fetchFlags}, C{fetchInternalDate},
3125
C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody}
3128
@type messages: C{MessageSet} or C{str}
3129
@param messages: A message sequence set
3132
@param uid: Indicates whether the message sequence set is of message
3133
numbers or of unique message IDs.
3136
@return: A deferred whose callback is invoked with a dict mapping
3137
message numbers to dict of the retrieved data values, or whose
3138
errback is invoked if there is an error. They dictionary keys
3139
are "flags", "date", "size", "envelope", and "body".
3142
messages, useUID=uid, flags=1, internaldate=1,
3143
rfc822size=1, envelope=1, body=1
3145
d.addCallback(self.__cbFetch)
3148
def fetchAll(self, messages, uid=0):
3149
"""Retrieve several different fields of one or more messages
3151
This command is allowed in the Selected state. This is equivalent
3152
to issuing all of the C{fetchFlags}, C{fetchInternalDate},
3153
C{fetchSize}, and C{fetchEnvelope} functions.
3155
@type messages: C{MessageSet} or C{str}
3156
@param messages: A message sequence set
3159
@param uid: Indicates whether the message sequence set is of message
3160
numbers or of unique message IDs.
3163
@return: A deferred whose callback is invoked with a dict mapping
3164
message numbers to dict of the retrieved data values, or whose
3165
errback is invoked if there is an error. They dictionary keys
3166
are "flags", "date", "size", and "envelope".
3169
messages, useUID=uid, flags=1, internaldate=1,
3170
rfc822size=1, envelope=1
3172
d.addCallback(self.__cbFetch)
3175
def fetchFast(self, messages, uid=0):
3176
"""Retrieve several different fields of one or more messages
3178
This command is allowed in the Selected state. This is equivalent
3179
to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and
3180
C{fetchSize} functions.
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 dict of the retrieved data values, or whose
3192
errback is invoked if there is an error. They dictionary keys are
3193
"flags", "date", and "size".
3196
messages, useUID=uid, flags=1, internaldate=1, rfc822size=1
3198
d.addCallback(self.__cbFetch)
3201
def __cbFetch(self, (lines, last)):
3204
parts = line.split(None, 2)
3206
if parts[1] == 'FETCH':
3210
raise IllegalServerResponse, line
3212
data = parseNestedParens(parts[2])
3213
while len(data) == 1 and isinstance(data, types.ListType):
3217
raise IllegalServerResponse("Not enough arguments", data)
3218
flags.setdefault(id, {})[data[0]] = data[1]
3221
print '(2)Ignoring ', parts
3223
print '(3)Ignoring ', parts
3226
def fetchSpecific(self, messages, uid=0, headerType=None,
3227
headerNumber=None, headerArgs=None, peek=None,
3228
offset=None, length=None):
3229
"""Retrieve a specific section of one or more messages
3231
@type messages: C{MessageSet} or C{str}
3232
@param messages: A message sequence set
3235
@param uid: Indicates whether the message sequence set is of message
3236
numbers or of unique message IDs.
3238
@type headerType: C{str}
3239
@param headerType: If specified, must be one of HEADER,
3240
HEADER.FIELDS, HEADER.FIELDS.NOT, MIME, or TEXT, and will determine
3241
which part of the message is retrieved. For HEADER.FIELDS and
3242
HEADER.FIELDS.NOT, C{headerArgs} must be a sequence of header names.
3243
For MIME, C{headerNumber} must be specified.
3245
@type headerNumber: C{int} or C{int} sequence
3246
@param headerNumber: The nested rfc822 index specifying the
3247
entity to retrieve. For example, C{1} retrieves the first
3248
entity of the message, and C{(2, 1, 3}) retrieves the 3rd
3249
entity inside the first entity inside the second entity of
3252
@type headerArgs: A sequence of C{str}
3253
@param headerArgs: If C{headerType} is HEADER.FIELDS, these are the
3254
headers to retrieve. If it is HEADER.FIELDS.NOT, these are the
3255
headers to exclude from retrieval.
3258
@param peek: If true, cause the server to not set the \\Seen
3259
flag on this message as a result of this command.
3261
@type offset: C{int}
3262
@param offset: The number of octets at the beginning of the result
3265
@type length: C{int}
3266
@param length: The number of octets to retrieve.
3269
@return: A deferred whose callback is invoked with a mapping of
3270
message numbers to retrieved data, or whose errback is invoked
3271
if there is an error.
3273
fmt = '%s BODY%s[%s%s%s]%s'
3274
if headerNumber is None:
3276
elif isinstance(headerNumber, types.IntType):
3277
number = str(headerNumber)
3279
number = '.'.join(headerNumber)
3280
if headerType is None:
3283
header = '.' + headerType
3287
if headerArgs is not None:
3288
payload = ' (%s)' % ' '.join(headerArgs)
3296
extra = '<%d.%d>' % (offset, length)
3297
fetch = uid and 'UID FETCH' or 'FETCH'
3298
cmd = fmt % (messages, peek and '.PEEK' or '', number, header, payload, extra)
3299
d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
3300
d.addCallback(self.__cbFetchSpecific)
3303
def __cbFetchSpecific(self, (lines, last)):
3306
parts = line.split(None, 2)
3308
if parts[1] == 'FETCH':
3312
raise IllegalServerResponse, line
3314
info[id] = parseNestedParens(parts[2])
3317
def _fetch(self, messages, useUID=0, **terms):
3318
fetch = useUID and 'UID FETCH' or 'FETCH'
3320
if 'rfc822text' in terms:
3321
del terms['rfc822text']
3322
terms['rfc822.text'] = True
3323
if 'rfc822size' in terms:
3324
del terms['rfc822size']
3325
terms['rfc822.size'] = True
3326
if 'rfc822header' in terms:
3327
del terms['rfc822header']
3328
terms['rfc822.header'] = True
3330
cmd = '%s (%s)' % (messages, ' '.join([s.upper() for s in terms.keys()]))
3331
d = self.sendCommand(Command(fetch, cmd, wantResponse=('FETCH',)))
3334
def setFlags(self, messages, flags, silent=1, uid=0):
3335
"""Set the flags for one or more messages.
3337
This command is allowed in the Selected state.
3339
@type messages: C{MessageSet} or C{str}
3340
@param messages: A message sequence set
3342
@type flags: Any iterable of C{str}
3343
@param flags: The flags to set
3345
@type silent: C{bool}
3346
@param silent: If true, cause the server to supress its verbose
3350
@param uid: Indicates whether the message sequence set is of message
3351
numbers or of unique message IDs.
3354
@return: A deferred whose callback is invoked with a list of the
3355
the server's responses (C{[]} if C{silent} is true) or whose
3356
errback is invoked if there is an error.
3358
return self._store(str(messages), silent and 'FLAGS.SILENT' or 'FLAGS', flags, uid)
3360
def addFlags(self, messages, flags, silent=1, uid=0):
3361
"""Add to the set flags for one or more messages.
3363
This command is allowed in the Selected state.
3365
@type messages: C{MessageSet} or C{str}
3366
@param messages: A message sequence set
3368
@type flags: Any iterable of C{str}
3369
@param flags: The flags to set
3371
@type silent: C{bool}
3372
@param silent: If true, cause the server to supress its verbose
3376
@param uid: Indicates whether the message sequence set is of message
3377
numbers or of unique message IDs.
3380
@return: A deferred whose callback is invoked with a list of the
3381
the server's responses (C{[]} if C{silent} is true) or whose
3382
errback is invoked if there is an error.
3384
return self._store(str(messages), silent and '+FLAGS.SILENT' or '+FLAGS', flags, uid)
3386
def removeFlags(self, messages, flags, silent=1, uid=0):
3387
"""Remove from the set flags for one or more messages.
3389
This command is allowed in the Selected state.
3391
@type messages: C{MessageSet} or C{str}
3392
@param messages: A message sequence set
3394
@type flags: Any iterable of C{str}
3395
@param flags: The flags to set
3397
@type silent: C{bool}
3398
@param silent: If true, cause the server to supress its verbose
3402
@param uid: Indicates whether the message sequence set is of message
3403
numbers or of unique message IDs.
3406
@return: A deferred whose callback is invoked with a list of the
3407
the server's responses (C{[]} if C{silent} is true) or whose
3408
errback is invoked if there is an error.
3410
return self._store(str(messages), silent and '-FLAGS.SILENT' or '-FLAGS', flags, uid)
3412
def _store(self, messages, cmd, flags, uid):
3413
store = uid and 'UID STORE' or 'STORE'
3414
args = ' '.join((messages, cmd, '(%s)' % ' '.join(flags)))
3415
d = self.sendCommand(Command(store, args, wantResponse=('FETCH',)))
3416
d.addCallback(self.__cbFetch)
3419
def copy(self, messages, mailbox, uid):
3420
"""Copy the specified messages to the specified mailbox.
3422
This command is allowed in the Selected state.
3424
@type messages: C{str}
3425
@param messages: A message sequence set
3427
@type mailbox: C{str}
3428
@param mailbox: The mailbox to which to copy the messages
3431
@param uid: If true, the C{messages} refers to message UIDs, rather
3432
than message sequence numbers.
3435
@return: A deferred whose callback is invoked with a true value
3436
when the copy is successful, or whose errback is invoked if there
3443
args = '%s %s' % (messages, mailbox.encode('imap4-utf-7'))
3444
return self.sendCommand(Command(cmd, args))
3447
# IMailboxListener methods
3449
def modeChanged(self, writeable):
3452
def flagsChanged(self, newFlags):
3455
def newMessages(self, exists, recent):
3459
class IllegalIdentifierError(IMAP4Exception): pass
3463
parts = s.split(',')
3466
low, high = p.split(':', 1)
3476
res.extend((low, high))
3478
raise IllegalIdentifierError(p)
3486
raise IllegalIdentifierError(p)
3491
class IllegalQueryError(IMAP4Exception): pass
3494
'ALL', 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'NEW', 'OLD', 'RECENT',
3495
'SEEN', 'UNANSWERED', 'UNDELETED', 'UNDRAFT', 'UNFLAGGED', 'UNSEEN'
3499
'LARGER', 'SMALLER', 'UID'
3502
def Query(sorted=0, **kwarg):
3503
"""Create a query string
3505
Among the accepted keywords are:
3507
all : If set to a true value, search all messages in the
3510
answered : If set to a true value, search messages flagged with
3513
bcc : A substring to search the BCC header field for
3515
before : Search messages with an internal date before this
3516
value. The given date should be a string in the format
3517
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3519
body : A substring to search the body of the messages for
3521
cc : A substring to search the CC header field for
3523
deleted : If set to a true value, search messages flagged with
3526
draft : If set to a true value, search messages flagged with
3529
flagged : If set to a true value, search messages flagged with
3532
from : A substring to search the From header field for
3534
header : A two-tuple of a header name and substring to search
3537
keyword : Search for messages with the given keyword set
3539
larger : Search for messages larger than this number of octets
3541
messages : Search only the given message sequence set.
3543
new : If set to a true value, search messages flagged with
3544
\\Recent but not \\Seen
3546
old : If set to a true value, search messages not flagged with
3549
on : Search messages with an internal date which is on this
3550
date. The given date should be a string in the format
3551
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3553
recent : If set to a true value, search for messages flagged with
3556
seen : If set to a true value, search for messages flagged with
3559
sentbefore : Search for messages with an RFC822 'Date' header before
3560
this date. The given date should be a string in the format
3561
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3563
senton : Search for messages with an RFC822 'Date' header which is
3564
on this date The given date should be a string in the format
3565
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3567
sentsince : Search for messages with an RFC822 'Date' header which is
3568
after this date. The given date should be a string in the format
3569
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3571
since : Search for messages with an internal date that is after
3572
this date.. The given date should be a string in the format
3573
of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
3575
smaller : Search for messages smaller than this number of octets
3577
subject : A substring to search the 'subject' header for
3579
text : A substring to search the entire message for
3581
to : A substring to search the 'to' header for
3583
uid : Search only the messages in the given message set
3585
unanswered : If set to a true value, search for messages not
3586
flagged with \\Answered
3588
undeleted : If set to a true value, search for messages not
3589
flagged with \\Deleted
3591
undraft : If set to a true value, search for messages not
3592
flagged with \\Draft
3594
unflagged : If set to a true value, search for messages not
3595
flagged with \\Flagged
3597
unkeyword : Search for messages without the given keyword set
3599
unseen : If set to a true value, search for messages not
3602
@type sorted: C{bool}
3603
@param sorted: If true, the output will be sorted, alphabetically.
3604
The standard does not require it, but it makes testing this function
3605
easier. The default is zero, and this should be acceptable for any
3609
@return: The formatted query string
3618
if k in _SIMPLE_BOOL and v:
3621
cmd.extend([k, v[0], '"%s"' % (v[1],)])
3622
elif k not in _NO_QUOTES:
3623
cmd.extend([k, '"%s"' % (v,)])
3625
cmd.extend([k, '%s' % (v,)])
3627
return '(%s)' % ' '.join(cmd)
3629
return ' '.join(cmd)
3632
"""The disjunction of two or more queries"""
3634
raise IllegalQueryError, args
3635
elif len(args) == 2:
3636
return '(OR %s %s)' % args
3638
return '(OR %s %s)' % (args[0], Or(*args[1:]))
3641
"""The negation of a query"""
3642
return '(NOT %s)' % (query,)
3644
class MismatchedNesting(IMAP4Exception):
3647
class MismatchedQuoting(IMAP4Exception):
3650
def wildcardToRegexp(wildcard, delim=None):
3651
wildcard = wildcard.replace('*', '(?:.*?)')
3653
wildcard = wildcard.replace('%', '(?:.*?)')
3655
wildcard = wildcard.replace('%', '(?:(?:[^%s])*?)' % re.escape(delim))
3656
return re.compile(wildcard, re.I)
3659
"""Split a string into whitespace delimited tokens
3661
Tokens that would otherwise be separated but are surrounded by \"
3662
remain as a single token. Any token that is not quoted and is
3663
equal to \"NIL\" is tokenized as C{None}.
3666
@param s: The string to be split
3668
@rtype: C{list} of C{str}
3669
@return: A list of the resulting tokens
3671
@raise MismatchedQuoting: Raised if an odd number of quotes are present
3675
inQuote = inWord = start = 0
3676
for (i, c) in zip(range(len(s)), s):
3677
if c == '"' and not inQuote:
3680
elif c == '"' and inQuote:
3682
result.append(s[start:i])
3684
elif not inWord and not inQuote and c not in ('"' + string.whitespace):
3687
elif inWord and not inQuote and c in string.whitespace:
3688
if s[start:i] == 'NIL':
3691
result.append(s[start:i])
3695
raise MismatchedQuoting(s)
3697
if s[start:] == 'NIL':
3700
result.append(s[start:])
3704
def splitOn(sequence, predicate, transformers):
3706
mode = predicate(sequence[0])
3708
for e in sequence[1:]:
3711
result.extend(transformers[mode](tmp))
3716
result.extend(transformers[mode](tmp))
3719
def collapseStrings(results):
3721
Turns a list of length-one strings and lists into a list of longer
3722
strings and lists. For example,
3724
['a', 'b', ['c', 'd']] is returned as ['ab', ['cd']]
3726
@type results: C{list} of C{str} and C{list}
3727
@param results: The list to be collapsed
3729
@rtype: C{list} of C{str} and C{list}
3730
@return: A new list which is the collapsed form of C{results}
3734
listsList = [isinstance(s, types.ListType) for s in results]
3736
pred = lambda e: isinstance(e, types.TupleType)
3738
0: lambda e: splitQuoted(''.join(e)),
3739
1: lambda e: [''.join([i[0] for i in e])]
3741
for (i, c, isList) in zip(range(len(results)), results, listsList):
3743
if begun is not None:
3744
copy.extend(splitOn(results[begun:i], pred, tran))
3746
copy.append(collapseStrings(c))
3749
if begun is not None:
3750
copy.extend(splitOn(results[begun:], pred, tran))
3754
def parseNestedParens(s, handleLiteral = 1):
3755
"""Parse an s-exp-like string into a more useful data structure.
3758
@param s: The s-exp-like string to parse
3760
@rtype: C{list} of C{str} and C{list}
3761
@return: A list containing the tokens present in the input.
3763
@raise MismatchedNesting: Raised if the number or placement
3764
of opening or closing parenthesis is invalid.
3776
contentStack[-1].append(s[i+1])
3780
inQuote = not inQuote
3781
contentStack[-1].append(c)
3785
contentStack[-1].append(c)
3786
inQuote = not inQuote
3788
elif handleLiteral and c == '{':
3789
end = s.find('}', i)
3791
raise ValueError, "Malformed literal"
3792
literalSize = int(s[i+1:end])
3793
contentStack[-1].append((s[end+3:end+3+literalSize],))
3794
i = end + 3 + literalSize
3795
elif c == '(' or c == '[':
3796
contentStack.append([])
3798
elif c == ')' or c == ']':
3799
contentStack[-2].append(contentStack.pop())
3802
contentStack[-1].append(c)
3805
raise MismatchedNesting(s)
3806
if len(contentStack) != 1:
3807
raise MismatchedNesting(s)
3808
return collapseStrings(contentStack[0])
3811
return '"%s"' % (s.replace('\\', '\\\\').replace('"', '\\"'),)
3814
return '{%d}\r\n%s' % (len(s), s)
3817
def __init__(self, value):
3821
return str(self.value)
3823
_ATOM_SPECIALS = '(){ %*"'
3828
if c < '\x20' or c > '\x7f':
3830
if c in _ATOM_SPECIALS:
3834
def _needsLiteral(s):
3835
# Change this to "return 1" to wig out stupid clients
3836
return '\n' in s or '\r' in s or len(s) > 1000
3838
def collapseNestedLists(items):
3839
"""Turn a nested list structure into an s-exp-like string.
3841
Strings in C{items} will be sent as literals if they contain CR or LF,
3842
otherwise they will be quoted. References to None in C{items} will be
3843
translated to the atom NIL. Objects with a 'read' attribute will have
3844
it called on them with no arguments and the returned string will be
3845
inserted into the output as a literal. Integers will be converted to
3846
strings and inserted into the output unquoted. Instances of
3847
C{DontQuoteMe} will be converted to strings and inserted into the output
3850
This function used to be much nicer, and only quote things that really
3851
needed to be quoted (and C{DontQuoteMe} did not exist), however, many
3852
broken IMAP4 clients were unable to deal with this level of sophistication,
3853
forcing the current behavior to be adopted for practical reasons.
3855
@type items: Any iterable
3862
pieces.extend([' ', 'NIL'])
3863
elif isinstance(i, (DontQuoteMe, int, long)):
3864
pieces.extend([' ', str(i)])
3865
elif isinstance(i, types.StringTypes):
3866
if _needsLiteral(i):
3867
pieces.extend([' ', '{', str(len(i)), '}', IMAP4Server.delimiter, i])
3869
pieces.extend([' ', _quote(i)])
3870
elif hasattr(i, 'read'):
3872
pieces.extend([' ', '{', str(len(d)), '}', IMAP4Server.delimiter, d])
3874
pieces.extend([' ', '(%s)' % (collapseNestedLists(i),)])
3875
return ''.join(pieces[1:])
3877
class IClientAuthentication(components.Interface):
3879
"""Return an identifier associated with this authentication scheme.
3884
def challengeResponse(self, secret, challenge):
3885
"""Generate a challenge response string"""
3887
class CramMD5ClientAuthenticator:
3888
__implements__ = (IClientAuthentication,)
3890
def __init__(self, user):
3896
def challengeResponse(self, secret, chal):
3897
response = hmac.HMAC(secret, chal).hexdigest()
3898
return '%s %s' % (self.user, response)
3900
class LOGINAuthenticator:
3901
__implements__ = (IClientAuthentication,)
3903
def __init__(self, user):
3909
def challengeResponse(self, secret, chal):
3910
if chal == 'User Name\0':
3912
elif chal == 'Password\0':
3915
class PLAINAuthenticator:
3916
__implements__ = (IClientAuthentication,)
3918
def __init__(self, user):
3924
def challengeResponse(self, secret, chal):
3925
return '%s\0%s\0' % (self.user, secret)
3927
class MailboxException(IMAP4Exception): pass
3929
class MailboxCollision(MailboxException):
3931
return 'Mailbox named %s already exists' % self.args
3933
class NoSuchMailbox(MailboxException):
3935
return 'No mailbox named %s exists' % self.args
3937
class ReadOnlyMailbox(MailboxException):
3939
return 'Mailbox open in read-only state'
3941
class IAccount(components.Interface):
3942
"""Interface for Account classes
3944
Implementors of this interface must also subclass
3945
C{twisted.cred.perspective.Perspective} and should consider implementing
3946
C{INamespacePresenter}.
3949
def addMailbox(self, name, mbox = None):
3950
"""Add a new mailbox to this account
3953
@param name: The name associated with this mailbox. It may not
3954
contain multiple hierarchical parts.
3956
@type mbox: An object implementing C{IMailbox}
3957
@param mbox: The mailbox to associate with this name. If C{None},
3958
a suitable default is created and used.
3960
@rtype: C{Deferred} or C{bool}
3961
@return: A true value if the creation succeeds, or a deferred whose
3962
callback will be invoked when the creation succeeds.
3964
@raise MailboxException: Raised if this mailbox cannot be added for
3965
some reason. This may also be raised asynchronously, if a C{Deferred}
3969
def create(self, pathspec):
3970
"""Create a new mailbox from the given hierarchical name.
3972
@type pathspec: C{str}
3973
@param pathspec: The full hierarchical name of a new mailbox to create.
3974
If any of the inferior hierarchical names to this one do not exist,
3975
they are created as well.
3977
@rtype: C{Deferred} or C{bool}
3978
@return: A true value if the creation succeeds, or a deferred whose
3979
callback will be invoked when the creation succeeds.
3981
@raise MailboxException: Raised if this mailbox cannot be added.
3982
This may also be raised asynchronously, if a C{Deferred} is
3986
def select(self, name, rw=True):
3987
"""Acquire a mailbox, given its name.
3990
@param name: The mailbox to acquire
3993
@param rw: If a true value, request a read-write version of this
3994
mailbox. If a false value, request a read-only version.
3996
@rtype: Any object implementing C{IMailbox} or C{Deferred}
3997
@return: The mailbox object, or a C{Deferred} whose callback will
3998
be invoked with the mailbox object. None may be returned if the
3999
specified mailbox may not be selected for any reason.
4002
def delete(self, name):
4003
"""Delete the mailbox with the specified name.
4006
@param name: The mailbox to delete.
4008
@rtype: C{Deferred} or C{bool}
4009
@return: A true value if the mailbox is successfully deleted, or a
4010
C{Deferred} whose callback will be invoked when the deletion
4013
@raise MailboxException: Raised if this mailbox cannot be deleted.
4014
This may also be raised asynchronously, if a C{Deferred} is returned.
4017
def rename(self, oldname, newname):
4020
@type oldname: C{str}
4021
@param oldname: The current name of the mailbox to rename.
4023
@type newname: C{str}
4024
@param newname: The new name to associate with the mailbox.
4026
@rtype: C{Deferred} or C{bool}
4027
@return: A true value if the mailbox is successfully renamed, or a
4028
C{Deferred} whose callback will be invoked when the rename operation
4031
@raise MailboxException: Raised if this mailbox cannot be
4032
renamed. This may also be raised asynchronously, if a C{Deferred}
4036
def isSubscribed(self, name):
4037
"""Check the subscription status of a mailbox
4040
@param name: The name of the mailbox to check
4042
@rtype: C{Deferred} or C{bool}
4043
@return: A true value if the given mailbox is currently subscribed
4044
to, a false value otherwise. A C{Deferred} may also be returned
4045
whose callback will be invoked with one of these values.
4048
def subscribe(self, name):
4049
"""Subscribe to a mailbox
4052
@param name: The name of the mailbox to subscribe to
4054
@rtype: C{Deferred} or C{bool}
4055
@return: A true value if the mailbox is subscribed to successfully,
4056
or a Deferred whose callback will be invoked with this value when
4057
the subscription is successful.
4059
@raise MailboxException: Raised if this mailbox cannot be
4060
subscribed to. This may also be raised asynchronously, if a
4061
C{Deferred} is returned.
4064
def unsubscribe(self, name):
4065
"""Unsubscribe from a mailbox
4068
@param name: The name of the mailbox to unsubscribe from
4070
@rtype: C{Deferred} or C{bool}
4071
@return: A true value if the mailbox is unsubscribed from successfully,
4072
or a Deferred whose callback will be invoked with this value when
4073
the unsubscription is successful.
4075
@raise MailboxException: Raised if this mailbox cannot be
4076
unsubscribed from. This may also be raised asynchronously, if a
4077
C{Deferred} is returned.
4080
def listMailboxes(self, ref, wildcard):
4081
"""List all the mailboxes that meet a certain criteria
4084
@param ref: The context in which to apply the wildcard
4086
@type wildcard: C{str}
4087
@param wildcard: An expression against which to match mailbox names.
4088
'*' matches any number of characters in a mailbox name, and '%'
4089
matches similarly, but will not match across hierarchical boundaries.
4091
@rtype: C{list} of C{tuple}
4092
@return: A list of C{(mailboxName, mailboxObject)} which meet the
4093
given criteria. A Deferred may also be returned.
4096
class INamespacePresenter(components.Interface):
4097
def getPersonalNamespaces(self):
4098
"""Report the available personal namespaces.
4100
Typically there should be only one personal namespace. A common
4101
name for it is \"\", and its hierarchical delimiter is usually
4104
@rtype: iterable of two-tuples of strings
4105
@return: The personal namespaces and their hierarchical delimiters.
4106
If no namespaces of this type exist, None should be returned.
4109
def getSharedNamespaces(self):
4110
"""Report the available shared namespaces.
4112
Shared namespaces do not belong to any individual user but are
4113
usually to one or more of them. Examples of shared namespaces
4114
might be \"#news\" for a usenet gateway.
4116
@rtype: iterable of two-tuples of strings
4117
@return: The shared namespaces and their hierarchical delimiters.
4118
If no namespaces of this type exist, None should be returned.
4121
def getUserNamespaces(self):
4122
"""Report the available user namespaces.
4124
These are namespaces that contain folders belonging to other users
4125
access to which this account has been granted.
4127
@rtype: iterable of two-tuples of strings
4128
@return: The user namespaces and their hierarchical delimiters.
4129
If no namespaces of this type exist, None should be returned.
4132
class MemoryAccount(perspective.Perspective):
4134
perspective.Perspective.__implements__,
4140
subscriptions = None
4143
def __init__(self, name):
4144
perspective.Perspective.__init__(self, name, name)
4146
self.subscriptions = []
4148
def allocateID(self):
4156
def addMailbox(self, name, mbox = None):
4158
if self.mailboxes.has_key(name):
4159
raise MailboxCollision, name
4161
mbox = self._emptyMailbox(name, self.allocateID())
4162
self.mailboxes[name] = mbox
4165
def create(self, pathspec):
4166
paths = filter(None, pathspec.split('/'))
4167
for accum in range(1, len(paths)):
4169
self.addMailbox('/'.join(paths[:accum]))
4170
except MailboxCollision:
4173
self.addMailbox('/'.join(paths))
4174
except MailboxCollision:
4175
if not pathspec.endswith('/'):
4179
def _emptyMailbox(self, name, id):
4180
raise NotImplementedError
4182
def select(self, name, readwrite=1):
4183
return self.mailboxes.get(name.upper())
4185
def delete(self, name):
4187
# See if this mailbox exists at all
4188
mbox = self.mailboxes.get(name)
4190
raise MailboxException("No such mailbox")
4191
# See if this box is flagged \Noselect
4192
if r'\Noselect' in mbox.getFlags():
4193
# Check for hierarchically inferior mailboxes with this one
4194
# as part of their root.
4195
for others in self.mailboxes.keys():
4196
if others != name and others.startswith(name):
4197
raise MailboxException, "Hierarchically inferior mailboxes exist and \\Noselect is set"
4200
# iff there are no hierarchically inferior names, we will
4201
# delete it from our ken.
4202
if self._inferiorNames(name) > 1:
4203
del self.mailboxes[name]
4205
def rename(self, oldname, newname):
4206
oldname = oldname.upper()
4207
newname = newname.upper()
4208
if not self.mailboxes.has_key(oldname):
4209
raise NoSuchMailbox, oldname
4211
inferiors = self._inferiorNames(oldname)
4212
inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
4214
for (old, new) in inferiors:
4215
if self.mailboxes.has_key(new):
4216
raise MailboxCollision, new
4218
for (old, new) in inferiors:
4219
self.mailboxes[new] = self.mailboxes[old]
4220
del self.mailboxes[old]
4222
def _inferiorNames(self, name):
4224
for infname in self.mailboxes.keys():
4225
if infname.startswith(name):
4226
inferiors.append(infname)
4229
def isSubscribed(self, name):
4230
return name.upper() in self.subscriptions
4232
def subscribe(self, name):
4234
if name not in self.subscriptions:
4235
self.subscriptions.append(name)
4237
def unsubscribe(self, name):
4239
if name not in self.subscriptions:
4240
raise MailboxException, "Not currently subscribed to " + name
4241
self.subscriptions.remove(name)
4243
def listMailboxes(self, ref, wildcard):
4244
ref = self._inferiorNames(ref.upper())
4245
wildcard = wildcardToRegexp(wildcard, '/')
4246
return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]
4249
## INamespacePresenter
4251
def getPersonalNamespaces(self):
4254
def getSharedNamespaces(self):
4257
def getOtherNamespaces(self):
4260
_statusRequestDict = {
4261
'MESSAGES': 'getMessageCount',
4262
'RECENT': 'getRecentCount',
4263
'UIDNEXT': 'getUIDNext',
4264
'UIDVALIDITY': 'getUIDValidity',
4265
'UNSEEN': 'getUnseenCount'
4267
def statusRequestHelper(mbox, names):
4270
r[n] = getattr(mbox, _statusRequestDict[n.upper()])()
4273
def parseAddr(addr):
4275
return [(None, None, None),]
4276
addrs = email.Utils.getaddresses([addr])
4277
return [[fn or None, None] + addr.split('@') for fn, addr in addrs]
4279
def getEnvelope(msg):
4280
headers = msg.getHeaders(True)
4281
date = headers.get('date')
4282
subject = headers.get('subject')
4283
from_ = headers.get('from')
4284
sender = headers.get('sender', from_)
4285
reply_to = headers.get('reply-to', from_)
4286
to = headers.get('to')
4287
cc = headers.get('cc')
4288
bcc = headers.get('bcc')
4289
in_reply_to = headers.get('in-reply-to')
4290
mid = headers.get('message-id')
4291
return (date, subject, parseAddr(from_), parseAddr(sender),
4292
reply_to and parseAddr(reply_to), to and parseAddr(to),
4293
cc and parseAddr(cc), bcc and parseAddr(bcc), in_reply_to, mid)
4295
def getLineCount(msg):
4296
# XXX - Super expensive, CACHE THIS VALUE FOR LATER RE-USE
4297
# XXX - This must be the number of lines in the ENCODED version
4299
for _ in msg.getBodyFile():
4303
def getBodyStructure(msg, extended=False):
4304
# XXX - This does not properly handle multipart messages
4305
# BODYSTRUCTURE is obscenely complex and criminally under-documented.
4308
headers = 'content-type', 'content-id', 'content-description', 'content-transfer-encoding'
4309
headers = msg.getHeaders(False, *headers)
4310
mm = headers.get('content-type')
4312
mm = ''.join(mm.splitlines())
4313
mimetype = mm.split(';')
4315
type = mimetype[0].split('/', 1)
4319
elif len(type) == 2:
4322
major = minor = None
4323
attrs = dict([x.strip().lower().split('=', 1) for x in mimetype[1:]])
4325
major = minor = None
4327
major = minor = None
4330
size = str(msg.getSize())
4332
major, minor, # Main and Sub MIME types
4333
attrs.items(), # content-type parameter list
4334
headers.get('content-id'), # Duh
4335
headers.get('content-description'), # Duh
4336
headers.get('content-transfer-encoding'), # Duh
4337
size, # Number of octets total
4340
if major is not None:
4341
if major.lower() == 'text':
4342
result.append(str(getLineCount(msg)))
4343
elif (major.lower(), minor.lower()) == ('message', 'rfc822'):
4344
contained = msg.getSubPart(0)
4345
result.append(getEnvelope(contained))
4346
result.append(getBodyStructure(contained, False))
4347
result.append(str(getLineCount(contained)))
4349
if not extended or major is None:
4352
if major.lower() != 'multipart':
4353
headers = 'content-md5', 'content-disposition', 'content-language'
4354
headers = msg.getHeaders(False, *headers)
4355
disp = headers.get('content-disposition')
4357
# XXX - I dunno if this is really right
4359
disp = disp.split('; ')
4361
disp = (disp[0].lower(), None)
4363
disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
4365
result.append(headers.get('content-md5'))
4367
result.append(headers.get('content-language'))
4373
submsg = msg.getSubPart(i)
4374
result.append(getBodyStructure(submsg))
4377
result.append(minor)
4378
result.append(attrs.items())
4380
# XXX - I dunno if this is really right
4381
headers = msg.getHeaders(False, 'content-disposition', 'content-language')
4382
disp = headers.get('content-disposition')
4384
disp = disp.split('; ')
4386
disp = (disp[0].lower(), None)
4388
disp = (disp[0].lower(), [x.split('=') for x in disp[1:]])
4391
result.append(headers.get('content-language'))
4395
class IMessagePart(components.Interface):
4396
def getHeaders(self, negate, *names):
4397
"""Retrieve a group of message headers.
4399
@type names: C{tuple} of C{str}
4400
@param names: The names of the headers to retrieve or omit.
4402
@type negate: C{bool}
4403
@param negate: If True, indicates that the headers listed in C{names}
4404
should be omitted from the return value, rather than included.
4407
@return: A mapping of header field names to header field values
4410
def getBodyFile(self):
4411
"""Retrieve a file object containing only the body of this message.
4415
"""Retrieve the total size, in octets, of this message.
4420
def isMultipart(self):
4421
"""Indicate whether this message has subparts.
4426
def getSubPart(self, part):
4427
"""Retrieve a MIME sub-message
4430
@param part: The number of the part to retrieve, indexed from 0.
4432
@raise C{IndexError}: Raised if the specified part does not exist.
4433
@raise C{TypeError}: Raised if this message is not multipart.
4435
@rtype: Any object implementing C{IMessagePart}.
4436
@return: The specified sub-part.
4439
class IMessage(IMessagePart):
4441
"""Retrieve the unique identifier associated with this message.
4445
"""Retrieve the flags associated with this message.
4448
@return: The flags, represented as strings.
4451
def getInternalDate(self):
4452
"""Retrieve the date internally associated with this message.
4455
@return: An RFC822-formatted date string.
4458
class IMessageFile(components.Interface):
4460
"""Return an file-like object opened for reading.
4462
Reading from the returned file will return all the bytes
4463
of which this message consists.
4466
class ISearchableMailbox(components.Interface):
4467
def search(self, query, uid):
4468
"""Search for messages that meet the given query criteria.
4470
If this interface is not implemented by the mailbox, L{IMailbox.fetch}
4471
and various methods of L{IMessage} will be used instead.
4473
Implementations which wish to offer better performance than the
4474
default implementation should implement this interface.
4476
@type query: C{list}
4477
@param query: The search criteria
4480
@param uid: If true, the IDs specified in the query are UIDs;
4481
otherwise they are message sequence IDs.
4483
@rtype: C{list} or C{Deferred}
4484
@return: A list of message sequence numbers or message UIDs which
4485
match the search criteria or a C{Deferred} whose callback will be
4486
invoked with such a list.
4489
class IMessageCopier(components.Interface):
4490
def copy(self, messageObject):
4491
"""Copy the given message object into this mailbox.
4493
The message object will be one which was previously returned by
4496
Implementations which wish to offer better performance than the
4497
default implementation should implement this interface.
4499
If this interface is not implemented by the mailbox, IMailbox.addMessage
4500
will be used instead.
4502
@rtype: C{Deferred} or C{int}
4503
@return: Either the UID of the message or a Deferred which fires
4504
with the UID when the copy finishes.
4507
class IMailboxInfo(components.Interface):
4508
"""Interface specifying only the methods required for C{listMailboxes}.
4510
Implementations can return objects implementing only these methods for
4511
return to C{listMailboxes} if it can allow them to operate more
4516
"""Return the flags defined in this mailbox
4518
Flags with the \\ prefix are reserved for use as system flags.
4520
@rtype: C{list} of C{str}
4521
@return: A list of the flags that can be set on messages in this mailbox.
4524
def getHierarchicalDelimiter(self):
4525
"""Get the character which delimits namespaces for in this mailbox.
4530
class IMailbox(IMailboxInfo):
4531
def getUIDValidity(self):
4532
"""Return the unique validity identifier for this mailbox.
4537
def getUIDNext(self):
4538
"""Return the likely UID for the next message added to this mailbox.
4543
def getUID(self, message):
4544
"""Return the UID of a message in the mailbox
4546
@type message: C{int}
4547
@param message: The message sequence number
4550
@return: The UID of the message.
4553
def getMessageCount(self):
4554
"""Return the number of messages in this mailbox.
4559
def getRecentCount(self):
4560
"""Return the number of messages with the 'Recent' flag.
4565
def getUnseenCount(self):
4566
"""Return the number of messages with the 'Unseen' flag.
4571
def isWriteable(self):
4572
"""Get the read/write status of the mailbox.
4575
@return: A true value if write permission is allowed, a false value otherwise.
4579
"""Called before this mailbox is deleted, permanently.
4581
If necessary, all resources held by this mailbox should be cleaned
4582
up here. This function _must_ set the \\Noselect flag on this
4586
def requestStatus(self, names):
4587
"""Return status information about this mailbox.
4589
Mailboxes which do not intend to do any special processing to
4590
generate the return value, C{statusRequestHelper} can be used
4591
to build the dictionary by calling the other interface methods
4592
which return the data for each name.
4594
@type names: Any iterable
4595
@param names: The status names to return information regarding.
4596
The possible values for each name are: MESSAGES, RECENT, UIDNEXT,
4597
UIDVALIDITY, UNSEEN.
4599
@rtype: C{dict} or C{Deferred}
4600
@return: A dictionary containing status information about the
4601
requested names is returned. If the process of looking this
4602
information up would be costly, a deferred whose callback will
4603
eventually be passed this dictionary is returned instead.
4606
def addListener(self, listener):
4607
"""Add a mailbox change listener
4609
@type listener: Any object which implements C{IMailboxListener}
4610
@param listener: An object to add to the set of those which will
4611
be notified when the contents of this mailbox change.
4614
def removeListener(self, listener):
4615
"""Remove a mailbox change listener
4617
@type listener: Any object previously added to and not removed from
4618
this mailbox as a listener.
4619
@param listener: The object to remove from the set of listeners.
4621
@raise ValueError: Raised when the given object is not a listener for
4625
def addMessage(self, message, flags = (), date = None):
4626
"""Add the given message to this mailbox.
4628
@type message: A file-like object
4629
@param message: The RFC822 formatted message
4631
@type flags: Any iterable of C{str}
4632
@param flags: The flags to associate with this message
4635
@param date: If specified, the date to associate with this
4639
@return: A deferred whose callback is invoked with the message
4640
id if the message is added successfully and whose errback is
4643
@raise ReadOnlyMailbox: Raised if this Mailbox is not open for
4648
"""Remove all messages flagged \\Deleted.
4650
@rtype: C{list} or C{Deferred}
4651
@return: The list of message sequence numbers which were deleted,
4652
or a C{Deferred} whose callback will be invoked with such a list.
4654
@raise ReadOnlyMailbox: Raised if this Mailbox is not open for
4658
def fetch(self, messages, uid):
4659
"""Retrieve one or more messages.
4661
@type messages: C{MessageSet}
4662
@param messages: The identifiers of messages to retrieve information
4666
@param uid: If true, the IDs specified in the query are UIDs;
4667
otherwise they are message sequence IDs.
4669
@rtype: Any iterable of two-tuples of message sequence numbers and
4670
implementors of C{IMessage}.
4673
def store(self, messages, flags, mode, uid):
4674
"""Set the flags of one or more messages.
4676
@type messages: A MessageSet object with the list of messages requested
4677
@param messages: The identifiers of the messages to set the flags of.
4679
@type flags: sequence of C{str}
4680
@param flags: The flags to set, unset, or add.
4682
@type mode: -1, 0, or 1
4683
@param mode: If mode is -1, these flags should be removed from the
4684
specified messages. If mode is 1, these flags should be added to
4685
the specified messages. If mode is 0, all existing flags should be
4686
cleared and these flags should be added.
4689
@param uid: If true, the IDs specified in the query are UIDs;
4690
otherwise they are message sequence IDs.
4692
@rtype: C{dict} or C{Deferred}
4693
@return: A C{dict} mapping message sequence numbers to sequences of C{str}
4694
representing the flags set on the message after this operation has
4695
been performed, or a C{Deferred} whose callback will be invoked with
4698
@raise ReadOnlyMailbox: Raised if this mailbox is not open for
4702
class ICloseableMailbox(components.Interface):
4703
"""A supplementary interface for mailboxes which require cleanup on close.
4705
Implementing this interface is optional. If it is implemented, the protocol
4706
code will call the close method defined whenever a mailbox is closed.
4709
"""Close this mailbox.
4711
@return: A C{Deferred} which fires when this mailbox
4712
has been closed, or None if the mailbox can be closed
4716
def _formatHeaders(headers):
4717
hdrs = [': '.join((k.title(), '\r\n'.join(v.splitlines()))) for (k, v)
4718
in headers.iteritems()]
4719
hdrs = '\r\n'.join(hdrs) + '\r\n'
4726
yield m.getSubPart(i)
4731
def iterateInReactor(i):
4732
"""Consume an interator at most a single iteration per reactor iteration.
4734
If the iterator produces a Deferred, the next iteration will not occur
4735
until the Deferred fires, otherwise the next iteration will be taken
4736
in the next reactor iteration.
4739
@return: A deferred which fires (with None) when the iterator is
4740
exhausted or whose errback is called if there is an exception.
4742
from twisted.internet import reactor
4743
d = defer.Deferred()
4747
except StopIteration:
4752
if isinstance(r, defer.Deferred):
4755
reactor.callLater(0, go, r)
4759
class MessageProducer:
4760
CHUNK_SIZE = 2 ** 2 ** 2 ** 2
4762
def __init__(self, msg, buffer = None):
4763
"""Produce this message.
4765
@param msg: The message I am to produce.
4766
@type msg: L{IMessage}
4768
@param buffer: A buffer to hold the message in. If None, I will
4769
use a L{tempfile.TemporaryFile}.
4770
@type buffer: file-like
4774
buffer = tempfile.TemporaryFile()
4775
self.buffer = buffer
4776
self.write = self.buffer.write
4778
def beginProducing(self, consumer):
4779
self.consumer = consumer
4780
return iterateInReactor(self._produce())
4783
headers = self.msg.getHeaders(True)
4785
if self.msg.isMultipart():
4786
content = headers.get('content-type')
4787
parts = [x.split('=', 1) for x in content.split(';')[1:]]
4788
parts = dict([(k.lower().strip(), v) for (k, v) in parts])
4789
boundary = parts.get('boundary')
4790
if boundary is None:
4792
boundary = '----=_%f_boundary_%f' % (time.time(), random.random())
4793
headers['content-type'] += '; boundary="%s"' % (boundary,)
4795
if boundary.startswith('"') and boundary.endswith('"'):
4796
boundary = boundary[1:-1]
4798
self.write(_formatHeaders(headers))
4800
if self.msg.isMultipart():
4801
for p in subparts(self.msg):
4802
self.write('\r\n--%s\r\n' % (boundary,))
4803
yield MessageProducer(p, self.buffer
4804
).beginProducing(None
4806
self.write('\r\n--%s--\r\n' % (boundary,))
4808
f = self.msg.getBodyFile()
4810
b = f.read(self.CHUNK_SIZE)
4812
self.buffer.write(b)
4817
self.buffer.seek(0, 0)
4818
yield FileProducer(self.buffer
4819
).beginProducing(self.consumer
4820
).addCallback(lambda _: self
4825
# Response should be a list of fields from the message:
4826
# date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
4829
# from, sender, reply-to, to, cc, and bcc are themselves lists of
4830
# address information:
4831
# personal name, source route, mailbox name, host name
4833
# reply-to and sender must not be None. If not present in a message
4834
# they should be defaulted to the value of the from field.
4836
__str__ = lambda self: 'envelope'
4840
__str__ = lambda self: 'flags'
4843
type = 'internaldate'
4844
__str__ = lambda self: 'internaldate'
4847
type = 'rfc822header'
4848
__str__ = lambda self: 'rfc822.header'
4852
__str__ = lambda self: 'rfc822.text'
4856
__str__ = lambda self: 'rfc822.size'
4860
__str__ = lambda self: 'rfc822'
4864
__str__ = lambda self: 'uid'
4875
partialLength = None
4880
part = '.'.join([str(x + 1) for x in self.part]) + '.'
4884
base += '[%s%s]' % (part, self.header,)
4886
base += '[%sTEXT]' % (part,)
4888
base += '[%sMIME]' % (part,)
4891
if self.partialBegin is not None:
4892
base += '<%d.%d>' % (self.partialBegin, self.partialLength)
4895
class BodyStructure:
4896
type = 'bodystructure'
4897
__str__ = lambda self: 'bodystructure'
4899
# These three aren't top-level, they don't need type indicators
4911
for f in self.fields:
4916
base += ' (%s)' % ' '.join(fields)
4918
base = '.'.join([str(x + 1) for x in self.part]) + '.' + base
4929
_simple_fetch_att = [
4930
('envelope', Envelope),
4932
('internaldate', InternalDate),
4933
('rfc822.header', RFC822Header),
4934
('rfc822.text', RFC822Text),
4935
('rfc822.size', RFC822Size),
4938
('bodystructure', BodyStructure),
4942
self.state = ['initial']
4946
def parseString(self, s):
4947
s = self.remaining + s
4949
while s or self.state:
4950
# print 'Entering state_' + self.state[-1] + ' with', repr(s)
4951
state = self.state.pop()
4953
s = s[getattr(self, 'state_' + state)(s):]
4955
self.state.append(state)
4960
def state_initial(self, s):
4961
# In the initial state, the literals "ALL", "FULL", and "FAST"
4962
# are accepted, as is a ( indicating the beginning of a fetch_att
4963
# token, as is the beginning of a fetch_att token.
4968
if l.startswith('all'):
4969
self.result.extend((
4970
self.Flags(), self.InternalDate(),
4971
self.RFC822Size(), self.Envelope()
4974
if l.startswith('full'):
4975
self.result.extend((
4976
self.Flags(), self.InternalDate(),
4977
self.RFC822Size(), self.Envelope(),
4981
if l.startswith('fast'):
4982
self.result.extend((
4983
self.Flags(), self.InternalDate(), self.RFC822Size(),
4987
if l.startswith('('):
4988
self.state.extend(('close_paren', 'maybe_fetch_att', 'fetch_att'))
4991
self.state.append('fetch_att')
4994
def state_close_paren(self, s):
4995
if s.startswith(')'):
4997
raise Exception("Missing )")
4999
def state_whitespace(self, s):
5000
# Eat up all the leading whitespace
5001
if not s or not s[0].isspace():
5002
raise Exception("Whitespace expected, none found")
5004
for i in range(len(s)):
5005
if not s[i].isspace():
5009
def state_maybe_fetch_att(self, s):
5010
if not s.startswith(')'):
5011
self.state.extend(('maybe_fetch_att', 'fetch_att', 'whitespace'))
5014
def state_fetch_att(self, s):
5015
# Allowed fetch_att tokens are "ENVELOPE", "FLAGS", "INTERNALDATE",
5016
# "RFC822", "RFC822.HEADER", "RFC822.SIZE", "RFC822.TEXT", "BODY",
5017
# "BODYSTRUCTURE", "UID",
5018
# "BODY [".PEEK"] [<section>] ["<" <number> "." <nz_number> ">"]
5021
for (name, cls) in self._simple_fetch_att:
5022
if l.startswith(name):
5023
self.result.append(cls())
5027
if l.startswith('body.peek'):
5030
elif l.startswith('body'):
5033
raise Exception("Nothing recognized in fetch_att: %s" % (l,))
5035
self.pending_body = b
5036
self.state.extend(('got_body', 'maybe_partial', 'maybe_section'))
5039
def state_got_body(self, s):
5040
self.result.append(self.pending_body)
5041
del self.pending_body
5044
def state_maybe_section(self, s):
5045
if not s.startswith("["):
5048
self.state.extend(('section', 'part_number'))
5051
def state_part_number(self, s):
5055
while dot != -1 and s[last + 1].isdigit():
5056
parts.append(int(s[last + 1:dot]) - 1)
5057
last, dot = dot, s.find('.', dot + 1)
5061
def state_section(self, s):
5062
# Grab [HEADER] or [HEADER.FIELDS (Header list)] or
5063
# [HEADER.FIELDS.NOT (Header list)], [TEXT], or [MIME]
5068
if l.startswith(']'):
5069
self.pending_body.empty = True
5071
elif l.startswith('header]'):
5072
self.pending_body.header = self.Header()
5074
elif l.startswith('text]'):
5075
self.pending_body.text = self.Text()
5077
elif l.startswith('mime]'):
5078
self.pending_body.mime = self.MIME()
5082
if l.startswith('header.fields.not'):
5085
elif l.startswith('header.fields'):
5088
raise Exception("Unhandled section contents")
5090
self.pending_body.header = h
5091
self.state.extend(('finish_section', 'header_list', 'whitespace'))
5092
self.pending_body.part = tuple(self.parts)
5096
def state_finish_section(self, s):
5097
if not s.startswith(']'):
5098
raise Exception("section must end with ]")
5101
def state_header_list(self, s):
5102
if not s.startswith('('):
5103
raise Exception("Header list must begin with (")
5106
raise Exception("Header list must end with )")
5108
headers = s[1:end].split()
5109
self.pending_body.header.fields = map(str.upper, headers)
5112
def state_maybe_partial(self, s):
5113
# Grab <number.number> or nothing at all
5114
if not s.startswith('<'):
5118
raise Exception("Found < but not >")
5121
parts = partial.split('.', 1)
5123
raise Exception("Partial specification did not include two .-delimited integers")
5124
begin, length = map(int, parts)
5125
self.pending_body.partialBegin = begin
5126
self.pending_body.partialLength = length
5131
CHUNK_SIZE = 2 ** 2 ** 2 ** 2
5135
def __init__(self, f):
5138
def beginProducing(self, consumer):
5139
self.consumer = consumer
5140
self.produce = consumer.write
5141
d = self._onDone = defer.Deferred()
5142
self.consumer.registerProducer(self, False)
5145
def resumeProducing(self):
5148
b = '{%d}\r\n' % self._size()
5149
self.firstWrite = False
5152
b = b + self.f.read(self.CHUNK_SIZE)
5154
self.consumer.unregisterProducer()
5155
self._onDone.callback(self)
5156
self._onDone = self.f = self.consumer = None
5160
def pauseProducing(self):
5163
def stopProducing(self):
5174
# XXX - This may require localization :(
5176
'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct',
5177
'nov', 'dec', 'january', 'february', 'march', 'april', 'may', 'june',
5178
'july', 'august', 'september', 'october', 'november', 'december'
5181
'day': r"(?P<day>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
5182
'mon': r"(?P<mon>\w+)",
5183
'year': r"(?P<year>\d\d\d\d)"
5185
m = re.match('%(day)s-%(mon)s-%(year)s' % expr, s)
5187
raise ValueError, "Cannot parse time string %r" % (s,)
5190
d['mon'] = 1 + (months.index(d['mon'].lower()) % 12)
5191
d['year'] = int(d['year'])
5192
d['day'] = int(d['day'])
5194
raise ValueError, "Cannot parse time string %r" % (s,)
5196
return time.struct_time(
5197
(d['year'], d['mon'], d['day'], 0, 0, 0, -1, -1, -1)
5201
def modified_base64(s):
5202
return binascii.b2a_base64(s)[:-1].rstrip('=').replace('/', ',')
5204
def modified_unbase64(s):
5205
return binascii.a2b_base64(s.replace(',', '/') + '===')
5211
if ord(c) in (range(0x20, 0x26) + range(0x27, 0x7f)):
5213
r.extend(['&', modified_base64(''.join(_in)), '-'])
5218
r.extend(['&', modified_base64(''.join(_in)), '-'])
5224
r.extend(['&', modified_base64(''.join(_in)), '-'])
5225
return (''.join(r), len(s))
5231
if c == '&' and not decode:
5233
elif c == '-' and decode:
5234
if len(decode) == 1:
5237
r.append(modified_unbase64(''.join(decode[1:])))
5244
r.append(modified_unbase64(''.join(decode[1:])))
5245
return (''.join(r), len(s))
5247
class StreamReader(codecs.StreamReader):
5248
def decode(self, s, errors='strict'):
5251
class StreamWriter(codecs.StreamWriter):
5252
def decode(self, s, errors='strict'):
5255
def imap4_utf_7(name):
5256
if name == 'imap4-utf-7':
5257
return (encoder, decoder, StreamReader, StreamWriter)
5258
codecs.register(imap4_utf_7)
5261
'IMAP4Server', 'IMAP4Client', 'IMAP4Exception', 'IllegalClientResponse',
5262
'IllegalOperation', 'IllegalMailboxEncoding', 'IMailboxListener',
5263
'UnhandledResponse', 'NegativeResponse', 'NoSupportedAuthentication',
5264
'IllegalServerResponse', 'IllegalIdentifierError', 'IllegalQueryError',
5265
'MismatchedNesting', 'MismatchedQuoting', 'IClientAuthentication',
5266
'CramMD5ClientAuthenticator', 'MailboxException', 'MailboxCollision',
5267
'NoSuchMailbox', 'ReadOnlyMailbox', 'IAccount', 'MemoryAccount',
5268
'IMailbox', 'statusRequestHelper', 'INamespacePresenter',