| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
""" |
|---|
| 7 |
An IMAP4 protocol implementation |
|---|
| 8 |
|
|---|
| 9 |
@author: Jp Calderone |
|---|
| 10 |
|
|---|
| 11 |
To do:: |
|---|
| 12 |
Suspend idle timeout while server is processing |
|---|
| 13 |
Use an async message parser instead of buffering in memory |
|---|
| 14 |
Figure out a way to not queue multi-message client requests (Flow? A simple callback?) |
|---|
| 15 |
Clarify some API docs (Query, etc) |
|---|
| 16 |
Make APPEND recognize (again) non-existent mailboxes before accepting the literal |
|---|
| 17 |
""" |
|---|
| 18 |
|
|---|
| 19 |
import rfc822 |
|---|
| 20 |
import base64 |
|---|
| 21 |
import binascii |
|---|
| 22 |
import hmac |
|---|
| 23 |
import re |
|---|
| 24 |
import tempfile |
|---|
| 25 |
import string |
|---|
| 26 |
import time |
|---|
| 27 |
import random |
|---|
| 28 |
import types |
|---|
| 29 |
|
|---|
| 30 |
import email.Utils |
|---|
| 31 |
|
|---|
| 32 |
try: |
|---|
| 33 |
import cStringIO as StringIO |
|---|
| 34 |
except: |
|---|
| 35 |
import StringIO |
|---|
| 36 |
|
|---|
| 37 |
from zope.interface import implements, Interface |
|---|
| 38 |
|
|---|
| 39 |
from twisted.protocols import basic |
|---|
| 40 |
from twisted.protocols import policies |
|---|
| 41 |
from twisted.internet import defer |
|---|
| 42 |
from twisted.internet import error |
|---|
| 43 |
from twisted.internet.defer import maybeDeferred |
|---|
| 44 |
from twisted.python import log, text |
|---|
| 45 |
from twisted.internet import interfaces |
|---|
| 46 |
|
|---|
| 47 |
from twisted import cred |
|---|
| 48 |
import twisted.cred.error |
|---|
| 49 |
import twisted.cred.credentials |
|---|
| 50 |
|
|---|
| 51 |
class MessageSet(object): |
|---|
| 52 |
""" |
|---|
| 53 |
Essentially an infinite bitfield, with some extra features. |
|---|
| 54 |
|
|---|
| 55 |
@type getnext: Function taking C{int} returning C{int} |
|---|
| 56 |
@ivar getnext: A function that returns the next message number, |
|---|
| 57 |
used when iterating through the MessageSet. By default, a function |
|---|
| 58 |
returning the next integer is supplied, but as this can be rather |
|---|
| 59 |
inefficient for sparse UID iterations, it is recommended to supply |
|---|
| 60 |
one when messages are requested by UID. The argument is provided |
|---|
| 61 |
as a hint to the implementation and may be ignored if it makes sense |
|---|
| 62 |
to do so (eg, if an iterator is being used that maintains its own |
|---|
| 63 |
state, it is guaranteed that it will not be called out-of-order). |
|---|
| 64 |
""" |
|---|
| 65 |
_empty = [] |
|---|
| 66 |
|
|---|
| 67 |
def __init__(self, start=_empty, end=_empty): |
|---|
| 68 |
""" |
|---|
| 69 |
Create a new MessageSet() |
|---|
| 70 |
|
|---|
| 71 |
@type start: Optional C{int} |
|---|
| 72 |
@param start: Start of range, or only message number |
|---|
| 73 |
|
|---|
| 74 |
@type end: Optional C{int} |
|---|
| 75 |
@param end: End of range. |
|---|
| 76 |
""" |
|---|
| 77 |
self._last = self._empty |
|---|
| 78 |
self.ranges = [] |
|---|
| 79 |
self.getnext = lambda x: x+1 |
|---|
| 80 |
|
|---|
| 81 |
|
|---|
| 82 |
if start is self._empty: |
|---|
| 83 |
return |
|---|
| 84 |
|
|---|
| 85 |
if isinstance(start, types.ListType): |
|---|
| 86 |
self.ranges = start[:] |
|---|
| 87 |
self.clean() |
|---|
| 88 |
else: |
|---|
| 89 |
self.add(start,end) |
|---|
| 90 |
|
|---|
| 91 |
|
|---|
| 92 |
def last(): |
|---|
| 93 |
def _setLast(self,value): |
|---|
| 94 |
if self._last is not self._empty: |
|---|
| 95 |
raise ValueError("last already set") |
|---|
| 96 |
|
|---|
| 97 |
self._last = value |
|---|
| 98 |
for i,(l,h) in enumerate(self.ranges): |
|---|
| 99 |
if l is not None: |
|---|
| 100 |
break |
|---|
| 101 |
l = value |
|---|
| 102 |
if h is None: |
|---|
| 103 |
h = value |
|---|
| 104 |
if l > h: |
|---|
| 105 |
l, h = h, l |
|---|
| 106 |
self.ranges[i] = (l,h) |
|---|
| 107 |
|
|---|
| 108 |
self.clean() |
|---|
| 109 |
|
|---|
| 110 |
def _getLast(self): |
|---|
| 111 |
return self._last |
|---|
| 112 |
|
|---|
| 113 |
doc = ''' |
|---|
| 114 |
"Highest" message number, refered to by "*". |
|---|
| 115 |
Must be set before attempting to use the MessageSet. |
|---|
| 116 |
''' |
|---|
| 117 |
return _getLast, _setLast, None, doc |
|---|
| 118 |
last = property(*last()) |
|---|
| 119 |
|
|---|
| 120 |
def add(self, start, end=_empty): |
|---|
| 121 |
""" |
|---|
| 122 |
Add another range |
|---|
| 123 |
|
|---|
| 124 |
@type start: C{int} |
|---|
| 125 |
@param start: Start of range, or only message number |
|---|
| 126 |
|
|---|
| 127 |
@type end: Optional C{int} |
|---|
| 128 |
@param end: End of range. |
|---|
| 129 |
""" |
|---|
| 130 |
if end is self._empty: |
|---|
| 131 |
end = start |
|---|
| 132 |
|
|---|
| 133 |
if self._last is not self._empty: |
|---|
| 134 |
if start is None: |
|---|
| 135 |
start = self.last |
|---|
| 136 |
if end is None: |
|---|
| 137 |
end = self.last |
|---|
| 138 |
|
|---|
| 139 |
if start > end: |
|---|
| 140 |
|
|---|
| 141 |
|
|---|
| 142 |
|
|---|
| 143 |
start, end = end, start |
|---|
| 144 |
|
|---|
| 145 |
self.ranges.append((start,end)) |
|---|
| 146 |
self.clean() |
|---|
| 147 |
|
|---|
| 148 |
def __add__(self, other): |
|---|
| 149 |
if isinstance(other, MessageSet): |
|---|
| 150 |
ranges = self.ranges + other.ranges |
|---|
| 151 |
return MessageSet(ranges) |
|---|
| 152 |
else: |
|---|
| 153 |
res = MessageSet(self.ranges) |
|---|
| 154 |
try: |
|---|
| 155 |
res.add(*other) |
|---|
| 156 |
except TypeError: |
|---|
| 157 |
res.add(other) |
|---|
| 158 |
return res |
|---|
| 159 |
|
|---|
| 160 |
def extend(self, other): |
|---|
| 161 |
if isinstance(other, MessageSet): |
|---|
| 162 |
self.ranges.extend(other.ranges) |
|---|
| 163 |
self.clean() |
|---|
| 164 |
else: |
|---|
| 165 |
try: |
|---|
| 166 |
self.add(*other) |
|---|
| 167 |
except TypeError: |
|---|
| 168 |
self.add(other) |
|---|
| 169 |
|
|---|
| 170 |
return self |
|---|
| 171 |
|
|---|
| 172 |
def clean(self): |
|---|
| 173 |
""" |
|---|
| 174 |
Clean ranges list, combining adjacent ranges |
|---|
| 175 |
""" |
|---|
| 176 |
|
|---|
| 177 |
self.ranges.sort() |
|---|
| 178 |
|
|---|
| 179 |
oldl, oldh = None, None |
|---|
| 180 |
for i,(l,h) in enumerate(self.ranges): |
|---|
| 181 |
if l is None: |
|---|
| 182 |
continue |
|---|
| 183 |
|
|---|
| 184 |
if oldl is not None and l <= oldh+1: |
|---|
| 185 |
l = oldl |
|---|
| 186 |
h = max(oldh,h) |
|---|
| 187 |
self.ranges[i-1] = None |
|---|
| 188 |
self.ranges[i] = (l,h) |
|---|
| 189 |
|
|---|
| 190 |
oldl,oldh = l,h |
|---|
| 191 |
|
|---|
| 192 |
self.ranges = filter(None, self.ranges) |
|---|
| 193 |
|
|---|
| 194 |
def __contains__(self, value): |
|---|
| 195 |
""" |
|---|
| 196 |
May raise TypeError if we encounter unknown "high" values |
|---|
| 197 |
""" |
|---|
| 198 |
for l,h in self.ranges: |
|---|
| 199 |
if l is None: |
|---|
| 200 |
raise TypeError( |
|---|
| 201 |
"Can't determine membership; last value not set") |
|---|
| 202 |
if l <= value <= h: |
|---|
| 203 |
return True |
|---|
| 204 |
|
|---|
| 205 |
return False |
|---|
| 206 |
|
|---|
| 207 |
def _iterator(self): |
|---|
| 208 |
for l,h in self.ranges: |
|---|
| 209 |
l = self.getnext(l-1) |
|---|
| 210 |
while l <= h: |
|---|
| 211 |
yield l |
|---|
| 212 |
l = self.getnext(l) |
|---|
| 213 |
if l is None: |
|---|
| 214 |
break |
|---|
| 215 |
|
|---|
| 216 |
def __iter__(self): |
|---|
| 217 |
if self.ranges and self.ranges[0][0] is None: |
|---|
| 218 |
raise TypeError("Can't iterate; last value not set") |
|---|
| 219 |
|
|---|
| 220 |
return self._iterator() |
|---|
| 221 |
|
|---|
| 222 |
def __len__(self): |
|---|
| 223 |
res = 0 |
|---|
| 224 |
for l, h in self.ranges: |
|---|
| 225 |
if l is None: |
|---|
| 226 |
raise TypeError("Can't size object; last value not set") |
|---|
| 227 |
res += (h - l) + 1 |
|---|
| 228 |
|
|---|
| 229 |
return res |
|---|
| 230 |
|
|---|
| 231 |
def __str__(self): |
|---|
| 232 |
p = [] |
|---|
| 233 |
for low, high in self.ranges: |
|---|
| 234 |
if low == high: |
|---|
| 235 |
if low is None: |
|---|
| 236 |
p.append('*') |
|---|
| 237 |
else: |
|---|
| 238 |
p.append(str(low)) |
|---|
| 239 |
elif low is None: |
|---|
| 240 |
p.append('%d:*' % (high,)) |
|---|
| 241 |
else: |
|---|
| 242 |
p.append('%d:%d' % (low, high)) |
|---|
| 243 |
return ','.join(p) |
|---|
| 244 |
|
|---|
| 245 |
def __repr__(self): |
|---|
| 246 |
return '<MessageSet %s>' % (str(self),) |
|---|
| 247 |
|
|---|
| 248 |
def __eq__(self, other): |
|---|
| 249 |
if isinstance(other, MessageSet): |
|---|
| 250 |
return self.ranges == other.ranges |
|---|
| 251 |
return False |
|---|
| 252 |
|
|---|
| 253 |
|
|---|
| 254 |
class LiteralString: |
|---|
| 255 |
def __init__(self, size, defered): |
|---|
| 256 |
self.size = size |
|---|
| 257 |
self.data = [] |
|---|
| 258 |
self.defer = defered |
|---|
| 259 |
|
|---|
| 260 |
def write(self, data): |
|---|
| 261 |
self.size -= len(data) |
|---|
| 262 |
passon = None |
|---|
| 263 |
if self.size > 0: |
|---|
| 264 |
self.data.append(data) |
|---|
| 265 |
else: |
|---|
| 266 |
if self.size: |
|---|
| 267 |
data, passon = data[:self.size], data[self.size:] |
|---|
| 268 |
else: |
|---|
| 269 |
passon = '' |
|---|
| 270 |
if data: |
|---|
| 271 |
self.data.append(data) |
|---|
| 272 |
return passon |
|---|
| 273 |
|
|---|
| 274 |
def callback(self, line): |
|---|
| 275 |
""" |
|---|
| 276 |
Call defered with data and rest of line |
|---|
| 277 |
""" |
|---|
| 278 |
self.defer.callback((''.join(self.data), line)) |
|---|
| 279 |
|
|---|
| 280 |
class LiteralFile: |
|---|
| 281 |
_memoryFileLimit = 1024 * 1024 * 10 |
|---|
| 282 |
|
|---|
| 283 |
def __init__(self, size, defered): |
|---|
| 284 |
self.size = size |
|---|
| 285 |
self.defer = defered |
|---|
| 286 |
if size > self._memoryFileLimit: |
|---|
| 287 |
self.data = tempfile.TemporaryFile() |
|---|
| 288 |
else: |
|---|
| 289 |
self.data = StringIO.StringIO() |
|---|
| 290 |
|
|---|
| 291 |
def write(self, data): |
|---|
| 292 |
self.size -= len(data) |
|---|
| 293 |
passon = None |
|---|
| 294 |
if self.size > 0: |
|---|
| 295 |
self.data.write(data) |
|---|
| 296 |
else: |
|---|
| 297 |
if self.size: |
|---|
| 298 |
data, passon = data[:self.size], data[self.size:] |
|---|
| 299 |
else: |
|---|
| 300 |
passon = '' |
|---|
| 301 |
if data: |
|---|
| 302 |
self.data.write(data) |
|---|
| 303 |
return passon |
|---|
| 304 |
|
|---|
| 305 |
def callback(self, line): |
|---|
| 306 |
""" |
|---|
| 307 |
Call defered with data and rest of line |
|---|
| 308 |
""" |
|---|
| 309 |
self.data.seek(0,0) |
|---|
| 310 |
self.defer.callback((self.data, line)) |
|---|
| 311 |
|
|---|
| 312 |
|
|---|
| 313 |
class WriteBuffer: |
|---|
| 314 |
"""Buffer up a bunch of writes before sending them all to a transport at once. |
|---|
| 315 |
""" |
|---|
| 316 |
def __init__(self, transport, size=8192): |
|---|
| 317 |
self.bufferSize = size |
|---|
| 318 |
self.transport = transport |
|---|
| 319 |
self._length = 0 |
|---|
| 320 |
self._writes = [] |
|---|
| 321 |
|
|---|
| 322 |
def write(self, s): |
|---|
| 323 |
self._length += len(s) |
|---|
| 324 |
self._writes.append(s) |
|---|
| 325 |
if self._length > self.bufferSize: |
|---|
| 326 |
self.flush() |
|---|
| 327 |
|
|---|
| 328 |
def flush(self): |
|---|
| 329 |
if self._writes: |
|---|
| 330 |
self.transport.writeSequence(self._writes) |
|---|
| 331 |
self._writes = [] |
|---|
| 332 |
self._length = 0 |
|---|
| 333 |
|
|---|
| 334 |
|
|---|
| 335 |
class Command: |
|---|
| 336 |
_1_RESPONSES = ('CAPABILITY', 'FLAGS', 'LIST', 'LSUB', 'STATUS', 'SEARCH', 'NAMESPACE') |
|---|
| 337 |
_2_RESPONSES = ('EXISTS', 'EXPUNGE', 'FETCH', 'RECENT') |
|---|
| 338 |
_OK_RESPONSES = ('UIDVALIDITY', 'UNSEEN', 'READ-WRITE', 'READ-ONLY', 'UIDNEXT', 'PERMANENTFLAGS') |
|---|
| 339 |
defer = None |
|---|
| 340 |
|
|---|
| 341 |
def __init__(self, command, args=None, wantResponse=(), |
|---|
| 342 |
continuation=None, *contArgs, **contKw): |
|---|
| 343 |
self.command = command |
|---|
| 344 |
self.args = args |
|---|
| 345 |
self.wantResponse = wantResponse |
|---|
| 346 |
self.continuation = lambda x: continuation(x, *contArgs, **contKw) |
|---|
| 347 |
self.lines = [] |
|---|
| 348 |
|
|---|
| 349 |
def format(self, tag): |
|---|
| 350 |
if self.args is None: |
|---|
| 351 |
return ' '.join((tag, self.command)) |
|---|
| 352 |
return ' '.join((tag, self.command, self.args)) |
|---|
| 353 |
|
|---|
| 354 |
def finish(self, lastLine, unusedCallback): |
|---|
| 355 |
send = [] |
|---|
| 356 |
unuse = [] |
|---|
| 357 |
for L in self.lines: |
|---|
| 358 |
names = parseNestedParens(L) |
|---|
| 359 |
N = len(names) |
|---|
| 360 |
if (N >= 1 and names[0] in self._1_RESPONSES or |
|---|
| 361 |
N >= 2 and names[1] in self._2_RESPONSES or |
|---|
| 362 |
N >= 2 and names[0] == 'OK' and isinstance(names[1], types.ListType) and names[1][0] in self._OK_RESPONSES): |
|---|
| 363 |
send.append(names) |
|---|
| 364 |
else: |
|---|
| 365 |
unuse.append(names) |
|---|
| 366 |
d, self.defer = self.defer, None |
|---|
| 367 |
d.callback((send, lastLine)) |
|---|
| 368 |
if unuse: |
|---|
| 369 |
unusedCallback(unuse) |
|---|
| 370 |
|
|---|
| 371 |
class LOGINCredentials(cred.credentials.UsernamePassword): |
|---|
| 372 |
def __init__(self): |
|---|
| 373 |
self.challenges = ['Password\0', 'User Name\0'] |
|---|
| 374 |
self.responses = ['password', 'username'] |
|---|
| 375 |
cred.credentials.UsernamePassword.__init__(self, None, None) |
|---|
| 376 |
|
|---|
| 377 |
def getChallenge(self): |
|---|
| 378 |
return self.challenges.pop() |
|---|
| 379 |
|
|---|
| 380 |
def setResponse(self, response): |
|---|
| 381 |
setattr(self, self.responses.pop(), response) |
|---|
| 382 |
|
|---|
| 383 |
def moreChallenges(self): |
|---|
| 384 |
return bool(self.challenges) |
|---|
| 385 |
|
|---|
| 386 |
class PLAINCredentials(cred.credentials.UsernamePassword): |
|---|
| 387 |
def __init__(self): |
|---|
| 388 |
cred.credentials.UsernamePassword.__init__(self, None, None) |
|---|
| 389 |
|
|---|
| 390 |
def getChallenge(self): |
|---|
| 391 |
return '' |
|---|
| 392 |
|
|---|
| 393 |
def setResponse(self, response): |
|---|
| 394 |
parts = response[:-1].split('\0', 1) |
|---|
| 395 |
if len(parts) != 2: |
|---|
| 396 |
raise IllegalClientResponse("Malformed Response - wrong number of parts") |
|---|
| 397 |
self.username, self.password = parts |
|---|
| 398 |
|
|---|
| 399 |
def moreChallenges(self): |
|---|
| 400 |
return False |
|---|
| 401 |
|
|---|
| 402 |
class IMAP4Exception(Exception): |
|---|
| 403 |
def __init__(self, *args): |
|---|
| 404 |
Exception.__init__(self, *args) |
|---|
| 405 |
|
|---|
| 406 |
class IllegalClientResponse(IMAP4Exception): pass |
|---|
| 407 |
|
|---|
| 408 |
class IllegalOperation(IMAP4Exception): pass |
|---|
| 409 |
|
|---|
| 410 |
class IllegalMailboxEncoding(IMAP4Exception): pass |
|---|
| 411 |
|
|---|
| 412 |
class IMailboxListener(Interface): |
|---|
| 413 |
"""Interface for objects interested in mailbox events""" |
|---|
| 414 |
|
|---|
| 415 |
def modeChanged(writeable): |
|---|
| 416 |
"""Indicates that the write status of a mailbox has changed. |
|---|
| 417 |
|
|---|
| 418 |
@type writeable: C{bool} |
|---|
| 419 |
@param writeable: A true value if write is now allowed, false |
|---|
| 420 |
otherwise. |
|---|
| 421 |
""" |
|---|
| 422 |
|
|---|
| 423 |
def flagsChanged(newFlags): |
|---|
| 424 |
"""Indicates that the flags of one or more messages have changed. |
|---|
| 425 |
|
|---|
| 426 |
@type newFlags: C{dict} |
|---|
| 427 |
@param newFlags: A mapping of message identifiers to tuples of flags |
|---|
| 428 |
now set on that message. |
|---|
| 429 |
""" |
|---|
| 430 |
|
|---|
| 431 |
def newMessages(exists, recent): |
|---|
| 432 |
"""Indicates that the number of messages in a mailbox has changed. |
|---|
| 433 |
|
|---|
| 434 |
@type exists: C{int} or C{None} |
|---|
| 435 |
@param exists: The total number of messages now in this mailbox. |
|---|
| 436 |
If the total number of messages has not changed, this should be |
|---|
| 437 |
C{None}. |
|---|
| 438 |
|
|---|
| 439 |
@type recent: C{int} |
|---|
| 440 |
@param recent: The number of messages now flagged \\Recent. |
|---|
| 441 |
If the number of recent messages has not changed, this should be |
|---|
| 442 |
C{None}. |
|---|
| 443 |
""" |
|---|
| 444 |
|
|---|
| 445 |
class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin): |
|---|
| 446 |
""" |
|---|
| 447 |
Protocol implementation for an IMAP4rev1 server. |
|---|
| 448 |
|
|---|
| 449 |
The server can be in any of four states: |
|---|
| 450 |
- Non-authenticated |
|---|
| 451 |
- Authenticated |
|---|
| 452 |
- Selected |
|---|
| 453 |
- Logout |
|---|
| 454 |
""" |
|---|
| 455 |
implements(IMailboxListener) |
|---|
| 456 |
|
|---|
| 457 |
|
|---|
| 458 |
IDENT = 'Twisted IMAP4rev1 Ready' |
|---|
| 459 |
|
|---|
| 460 |
|
|---|
| 461 |
|
|---|
| 462 |
timeOut = 60 |
|---|
| 463 |
|
|---|
| 464 |
POSTAUTH_TIMEOUT = 60 * 30 |
|---|
| 465 |
|
|---|
| 466 |
|
|---|
| 467 |
startedTLS = False |
|---|
| 468 |
|
|---|
| 469 |
|
|---|
| 470 |
canStartTLS = False |
|---|
| 471 |
|
|---|
| 472 |
|
|---|
| 473 |
tags = None |
|---|
| 474 |
|
|---|
| 475 |
|
|---|
| 476 |
portal = None |
|---|
| 477 |
|
|---|
| 478 |
|
|---|
| 479 |
account = None |
|---|
| 480 |
|
|---|
| 481 |
|
|---|
| 482 |
_onLogout = None |
|---|
| 483 |
|
|---|
| 484 |
|
|---|
| 485 |
mbox = None |
|---|
| 486 |
|
|---|
| 487 |
|
|---|
| 488 |
_pendingLiteral = None |
|---|
| 489 |
|
|---|
| 490 |
|
|---|
| 491 |
_literalStringLimit = 4096 |
|---|
| 492 |
|
|---|
| 493 |
|
|---|
| 494 |
challengers = None |
|---|
| 495 |
|
|---|
| 496 |
state = 'unauth' |
|---|
| 497 |
|
|---|
| 498 |
parseState = 'command' |
|---|
| 499 |
|
|---|
| 500 |
def __init__(self, chal = None, contextFactory = None, scheduler = None): |
|---|
| 501 |
if chal is None: |
|---|
| 502 |
chal = {} |
|---|
| 503 |
self.challengers = chal |
|---|
| 504 |
self.ctx = contextFactory |
|---|
| 505 |
if scheduler is None: |
|---|
| 506 |
scheduler = iterateInReactor |
|---|
| 507 |
self._scheduler = scheduler |
|---|
| 508 |
self._queuedAsync = [] |
|---|
| 509 |
|
|---|
| 510 |
def capabilities(self): |
|---|
| 511 |
cap = {'AUTH': self.challengers.keys()} |
|---|
| 512 |
if self.ctx and self.canStartTLS: |
|---|
| 513 |
if not self.startedTLS and interfaces.ISSLTransport(self.transport, None) is None: |
|---|
| 514 |
cap['LOGINDISABLED'] = None |
|---|
| 515 |
cap['STARTTLS'] = None |
|---|
| 516 |
cap['NAMESPACE'] = None |
|---|
| 517 |
cap['IDLE'] = None |
|---|
| 518 |
return cap |
|---|
| 519 |
|
|---|
| 520 |
def connectionMade(self): |
|---|
| 521 |
self.tags = {} |
|---|
| 522 |
self.canStartTLS = interfaces.ITLSTransport(self.transport, None) is not None |
|---|
| 523 |
self.setTimeout(self.timeOut) |
|---|
| 524 |
self.sendServerGreeting() |
|---|
| 525 |
|
|---|
| 526 |
def connectionLost(self, reason): |
|---|
| 527 |
self.setTimeout(None) |
|---|
| 528 |
if self._onLogout: |
|---|
| 529 |
self._onLogout() |
|---|
| 530 |
self._onLogout = None |
|---|
| 531 |
|
|---|
| 532 |
def timeoutConnection(self): |
|---|
| 533 |
self.sendLine('* BYE Autologout; connection idle too long') |
|---|
| 534 |
self.transport.loseConnection() |
|---|
| 535 |
if self.mbox: |
|---|
| 536 |
self.mbox.removeListener(self) |
|---|
| 537 |
cmbx = ICloseableMailbox(self.mbox, None) |
|---|
| 538 |
if cmbx is not None: |
|---|
| 539 |
maybeDeferred(cmbx.close).addErrback(log.err) |
|---|
| 540 |
self.mbox = None |
|---|
| 541 |
self.state = 'timeout' |
|---|
| 542 |
|
|---|
| 543 |
def rawDataReceived(self, data): |
|---|
| 544 |
self.resetTimeout() |
|---|
| 545 |
passon = self._pendingLiteral.write(data) |
|---|
| 546 |
if passon is not None: |
|---|
| 547 |
self.setLineMode(passon) |
|---|
| 548 |
|
|---|
| 549 |
|
|---|
| 550 |
|
|---|
| 551 |
blocked = None |
|---|
| 552 |
|
|---|
| 553 |
def _unblock(self): |
|---|
| 554 |
commands = self.blocked |
|---|
| 555 |
self.blocked = None |
|---|
| 556 |
while commands and self.blocked is None: |
|---|
| 557 |
self.lineReceived(commands.pop(0)) |
|---|
| 558 |
if self.blocked is not None: |
|---|
| 559 |
self.blocked.extend(commands) |
|---|
| 560 |
|
|---|
| 561 |
|
|---|
| 562 |
|
|---|
| 563 |
|
|---|
| 564 |
|
|---|
| 565 |
def lineReceived(self, line): |
|---|
| 566 |
|
|---|
| 567 |
if self.blocked is not None: |
|---|
| 568 |
self.blocked.append(line) |
|---|
| 569 |
return |
|---|
| 570 |
|
|---|
| 571 |
self.resetTimeout() |
|---|
| 572 |
|
|---|
| 573 |
f = getattr(self, 'parse_' + self.parseState) |
|---|
| 574 |
try: |
|---|
| 575 |
f(line) |
|---|
| 576 |
except Exception, e: |
|---|
| 577 |
self.sendUntaggedResponse('BAD Server error: ' + str(e)) |
|---|
| 578 |
log.err() |
|---|
| 579 |
|
|---|
| 580 |
def parse_command(self, line): |
|---|
| 581 |
args = line.split(None, 2) |
|---|
| 582 |
rest = None |
|---|
| 583 |
if len(args) == 3: |
|---|
| 584 |
tag, cmd, rest = args |
|---|
| 585 |
elif len(args) == 2: |
|---|
| 586 |
tag, cmd = args |
|---|
| 587 |
elif len(args) == 1: |
|---|
| 588 |
tag = args[0] |
|---|
| 589 |
self.sendBadResponse(tag, 'Missing command') |
|---|
| 590 |
return None |
|---|
| 591 |
else: |
|---|
| 592 |
self.sendBadResponse(None, 'Null command') |
|---|
| 593 |
return None |
|---|
| 594 |
|
|---|
| 595 |
cmd = cmd.upper() |
|---|
| 596 |
try: |
|---|
| 597 |
return self.dispatchCommand(tag, cmd, rest) |
|---|
| 598 |
except IllegalClientResponse, e: |
|---|
| 599 |
self.sendBadResponse(tag, 'Illegal syntax: ' + str(e)) |
|---|
| 600 |
except IllegalOperation, e: |
|---|
| 601 |
self.sendNegativeResponse(tag, 'Illegal operation: ' + str(e)) |
|---|
| 602 |
except IllegalMailboxEncoding, e: |
|---|
| 603 |
self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + str(e)) |
|---|
| 604 |
|
|---|
| 605 |
def parse_pending(self, line): |
|---|
| 606 |
d = self._pendingLiteral |
|---|
| 607 |
self._pendingLiteral = None |
|---|
| 608 |
self.parseState = 'command' |
|---|
| 609 |
d.callback(line) |
|---|
| 610 |
|
|---|
| 611 |
def dispatchCommand(self, tag, cmd, rest, uid=None): |
|---|
| 612 |
f = self.lookupCommand(cmd) |
|---|
| 613 |
if f: |
|---|
| 614 |
fn = f[0] |
|---|
| 615 |
parseargs = f[1:] |
|---|
| 616 |
self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid) |
|---|
| 617 |
else: |
|---|
| 618 |
self.sendBadResponse(tag, 'Unsupported command') |
|---|
| 619 |
|
|---|
| 620 |
def lookupCommand(self, cmd): |
|---|
| 621 |
return getattr(self, '_'.join((self.state, cmd.upper())), None) |
|---|
| 622 |
|
|---|
| 623 |
def __doCommand(self, tag, handler, args, parseargs, line, uid): |
|---|
| 624 |
for (i, arg) in enumerate(parseargs): |
|---|
| 625 |
if callable(arg): |
|---|
| 626 |
parseargs = parseargs[i+1:] |
|---|
| 627 |
maybeDeferred(arg, self, line).addCallback( |
|---|
| 628 |
self.__cbDispatch, tag, handler, args, |
|---|
| 629 |
parseargs, uid).addErrback(self.__ebDispatch, tag) |
|---|
| 630 |
return |
|---|
| 631 |
else: |
|---|
| 632 |
args.append(arg) |
|---|
| 633 |
|
|---|
| 634 |
if line: |
|---|
| 635 |
|
|---|
| 636 |
raise IllegalClientResponse("Too many arguments for command: " + repr(line)) |
|---|
| 637 |
|
|---|
| 638 |
if uid is not None: |
|---|
| 639 |
handler(uid=uid, *args) |
|---|
| 640 |
else: |
|---|
| 641 |
handler(*args) |
|---|
| 642 |
|
|---|
| 643 |
def __cbDispatch(self, (arg, rest), tag, fn, args, parseargs, uid): |
|---|
| 644 |
args.append(arg) |
|---|
| 645 |
self.__doCommand(tag, fn, args, parseargs, rest, uid) |
|---|
| 646 |
|
|---|
| 647 |
def __ebDispatch(self, failure, tag): |
|---|
| 648 |
if failure.check(IllegalClientResponse): |
|---|
| 649 |
self.sendBadResponse(tag, 'Illegal syntax: ' + str(failure.value)) |
|---|
| 650 |
elif failure.check(IllegalOperation): |
|---|
| 651 |
self.sendNegativeResponse(tag, 'Illegal operation: ' + |
|---|
| 652 |
str(failure.value)) |
|---|
| 653 |
elif failure.check(IllegalMailboxEncoding): |
|---|
| 654 |
self.sendNegativeResponse(tag, 'Illegal mailbox name: ' + |
|---|
| 655 |
str(failure.value)) |
|---|
| 656 |
else: |
|---|
| 657 |
self.sendBadResponse(tag, 'Server error: ' + str(failure.value)) |
|---|
| 658 |
log.err(failure) |
|---|
| 659 |
|
|---|
| 660 |
def _stringLiteral(self, size): |
|---|
| 661 |
if size > self._literalStringLimit: |
|---|
| 662 |
raise IllegalClientResponse( |
|---|
| 663 |
"Literal too long! I accept at most %d octets" % |
|---|
| 664 |
(self._literalStringLimit,)) |
|---|
| 665 |
d = defer.Deferred() |
|---|
| 666 |
self.parseState = 'pending' |
|---|
| 667 |
self._pendingLiteral = LiteralString(size, d) |
|---|
| 668 |
self.sendContinuationRequest('Ready for %d octets of text' % size) |
|---|
| 669 |
self.setRawMode() |
|---|
| 670 |
return d |
|---|
| 671 |
|
|---|
| 672 |
def _fileLiteral(self, size): |
|---|
| 673 |
d = defer.Deferred() |
|---|
| 674 |
self.parseState = 'pending' |
|---|
| 675 |
self._pendingLiteral = LiteralFile(size, d) |
|---|
| 676 |
self.sendContinuationRequest('Ready for %d octets of data' % size) |
|---|
| 677 |
self.setRawMode() |
|---|
| 678 |
return d |
|---|
| 679 |
|
|---|
| 680 |
def arg_astring(self, line): |
|---|
| 681 |
""" |
|---|
| 682 |
Parse an astring from the line, return (arg, rest), possibly |
|---|
| 683 |
via a deferred (to handle literals) |
|---|
| 684 |
""" |
|---|
| 685 |
line = line.strip() |
|---|
| 686 |
if not line: |
|---|
| 687 |
raise IllegalClientResponse("Missing argument") |
|---|
| 688 |
d = None |
|---|
| 689 |
arg, rest = None, None |
|---|
| 690 |
if line[0] == '"': |
|---|
| 691 |
try: |
|---|
| 692 |
spam, arg, rest = line.split('"',2) |
|---|
| 693 |
rest = rest[1:] |
|---|
| 694 |
except ValueError: |
|---|
| 695 |
raise IllegalClientResponse("Unmatched quotes") |
|---|
| 696 |
elif line[0] == '{': |
|---|
| 697 |
|
|---|
| 698 |
if line[-1] != '}': |
|---|
| 699 |
raise IllegalClientResponse("Malformed literal") |
|---|
| 700 |
try: |
|---|
| 701 |
size = int(line[1:-1]) |
|---|
| 702 |
except ValueError: |
|---|
| 703 |
raise IllegalClientResponse("Bad literal size: " + line[1:-1]) |
|---|
| 704 |
d = self._stringLiteral(size) |
|---|
| 705 |
else: |
|---|
| 706 |
arg = line.split(' ',1) |
|---|
| 707 |
if len(arg) == 1: |
|---|
| 708 |
arg.append('') |
|---|
| 709 |
arg, rest = arg |
|---|
| 710 |
return d or (arg, rest) |
|---|
| 711 |
|
|---|
| 712 |
|
|---|
| 713 |
atomre = re.compile(r'(?P<atom>[^\](){%*"\\\x00-\x20\x80-\xff]+)( (?P<rest>.*$)|$)') |
|---|
| 714 |
|
|---|
| 715 |
def arg_atom(self, line): |
|---|
| 716 |
""" |
|---|
| 717 |
Parse an atom from the line |
|---|
| 718 |
""" |
|---|
| 719 |
if not line: |
|---|
| 720 |
raise IllegalClientResponse("Missing argument") |
|---|
| 721 |
m = self.atomre.match(line) |
|---|
| 722 |
if m: |
|---|
| 723 |
return m.group('atom'), m.group('rest') |
|---|
| 724 |
else: |
|---|
| 725 |
raise IllegalClientResponse("Malformed ATOM") |
|---|
| 726 |
|
|---|
| 727 |
def arg_plist(self, line): |
|---|
| 728 |
""" |
|---|
| 729 |
Parse a (non-nested) parenthesised list from the line |
|---|
| 730 |
""" |
|---|
| 731 |
if not line: |
|---|
| 732 |
raise IllegalClientResponse("Missing argument") |
|---|
| 733 |
|
|---|
| 734 |
if line[0] != "(": |
|---|
| 735 |
raise IllegalClientResponse("Missing parenthesis") |
|---|
| 736 |
|
|---|
| 737 |
i = line.find(")") |
|---|
| 738 |
|
|---|
| 739 |
if i == -1: |
|---|
| 740 |
raise IllegalClientResponse("Mismatched parenthesis") |
|---|
| 741 |
|
|---|
| 742 |
return (parseNestedParens(line[1:i],0), line[i+2:]) |
|---|
| 743 |
|
|---|
| 744 |
def arg_literal(self, line): |
|---|
| 745 |
""" |
|---|
| 746 |
Parse a literal from the line |
|---|
| 747 |
""" |
|---|
| 748 |
if not line: |
|---|
| 749 |
raise IllegalClientResponse("Missing argument") |
|---|
| 750 |
|
|---|
| 751 |
if line[0] != '{': |
|---|
| 752 |
raise IllegalClientResponse("Missing literal") |
|---|
| 753 |
|
|---|
| 754 |
if line[-1] != '}': |
|---|
| 755 |
raise IllegalClientResponse("Malformed literal") |
|---|
| 756 |
|
|---|
| 757 |
try: |
|---|
| 758 |
size = int(line[1:-1]) |
|---|
| 759 |
except ValueError: |
|---|
| 760 |
raise IllegalClientResponse("Bad literal size: " + line[1:-1]) |
|---|
| 761 |
|
|---|
| 762 |
return self._fileLiteral(size) |
|---|
| 763 |
|
|---|
| 764 |
def arg_searchkeys(self, line): |
|---|
| 765 |
""" |
|---|
| 766 |
searchkeys |
|---|
| 767 |
""" |
|---|
| 768 |
query = parseNestedParens(line) |
|---|
| 769 |
|
|---|
| 770 |
|
|---|
| 771 |
|
|---|
| 772 |
return (query, '') |
|---|
| 773 |
|
|---|
| 774 |
def arg_seqset(self, line): |
|---|
| 775 |
""" |
|---|
| 776 |
sequence-set |
|---|
| 777 |
""" |
|---|
| 778 |
rest = '' |
|---|
| 779 |
arg = line.split(' ',1) |
|---|
| 780 |
if len(arg) == 2: |
|---|
| 781 |
rest = arg[1] |
|---|
| 782 |
arg = arg[0] |
|---|
| 783 |
|
|---|
| 784 |
try: |
|---|
| 785 |
return (parseIdList(arg), rest) |
|---|
| 786 |
except IllegalIdentifierError, e: |
|---|
| 787 |
raise IllegalClientResponse("Bad message number " + str(e)) |
|---|
| 788 |
|
|---|
| 789 |
def arg_fetchatt(self, line): |
|---|
| 790 |
""" |
|---|
| 791 |
fetch-att |
|---|
| 792 |
""" |
|---|
| 793 |
p = _FetchParser() |
|---|
| 794 |
p.parseString(line) |
|---|
| 795 |
return (p.result, '') |
|---|
| 796 |
|
|---|
| 797 |
def arg_flaglist(self, line): |
|---|
| 798 |
""" |
|---|
| 799 |
Flag part of store-att-flag |
|---|
| 800 |
""" |
|---|
| 801 |
flags = [] |
|---|
| 802 |
if line[0] == '(': |
|---|
| 803 |
if line[-1] != ')': |
|---|
| 804 |
raise IllegalClientResponse("Mismatched parenthesis") |
|---|
| 805 |
line = line[1:-1] |
|---|
| 806 |
|
|---|
| 807 |
while line: |
|---|
| 808 |
m = self.atomre.search(line) |
|---|
| 809 |
if not m: |
|---|
| 810 |
raise IllegalClientResponse("Malformed flag") |
|---|
| 811 |
if line[0] == '\\' and m.start() == 1: |
|---|
| 812 |
flags.append('\\' + m.group('atom')) |
|---|
| 813 |
elif m.start() == 0: |
|---|
| 814 |
flags.append(m.group('atom')) |
|---|
| 815 |
else: |
|---|
| 816 |
raise IllegalClientResponse("Malformed flag") |
|---|
| 817 |
line = m.group('rest') |
|---|
| 818 |
|
|---|
| 819 |
return (flags, '') |
|---|
| 820 |
|
|---|
| 821 |
def arg_line(self, line): |
|---|
| 822 |
""" |
|---|
| 823 |
Command line of UID command |
|---|
| 824 |
""" |
|---|
| 825 |
return (line, '') |
|---|
| 826 |
|
|---|
| 827 |
def opt_plist(self, line): |
|---|
| 828 |
""" |
|---|
| 829 |
Optional parenthesised list |
|---|
| 830 |
""" |
|---|
| 831 |
if line.startswith('('): |
|---|
| 832 |
return self.arg_plist(line) |
|---|
| 833 |
else: |
|---|
| 834 |
return (None, line) |
|---|
| 835 |
|
|---|
| 836 |
def opt_datetime(self, line): |
|---|
| 837 |
""" |
|---|
| 838 |
Optional date-time string |
|---|
| 839 |
""" |
|---|
| 840 |
if line.startswith('"'): |
|---|
| 841 |
try: |
|---|
| 842 |
spam, date, rest = line.split('"',2) |
|---|
| 843 |
except IndexError: |
|---|
| 844 |
raise IllegalClientResponse("Malformed date-time") |
|---|
| 845 |
return (date, rest[1:]) |
|---|
| 846 |
else: |
|---|
| 847 |
return (None, line) |
|---|
| 848 |
|
|---|
| 849 |
def opt_charset(self, line): |
|---|
| 850 |
""" |
|---|
| 851 |
Optional charset of SEARCH command |
|---|
| 852 |
""" |
|---|
| 853 |
if line[:7].upper() == 'CHARSET': |
|---|
| 854 |
arg = line.split(' ',2) |
|---|
| 855 |
if len(arg) == 1: |
|---|
| 856 |
raise IllegalClientResponse("Missing charset identifier") |
|---|
| 857 |
if len(arg) == 2: |
|---|
| 858 |
arg.append('') |
|---|
| 859 |
spam, arg, rest = arg |
|---|
| 860 |
return (arg, rest) |
|---|
| 861 |
else: |
|---|
| 862 |
return (None, line) |
|---|
| 863 |
|
|---|
| 864 |
def sendServerGreeting(self): |
|---|
| 865 |
msg = '[CAPABILITY %s] %s' % (' '.join(self.listCapabilities()), self.IDENT) |
|---|
| 866 |
self.sendPositiveResponse(message=msg) |
|---|
| 867 |
|
|---|
| 868 |
def sendBadResponse(self, tag = None, message = ''): |
|---|
| 869 |
self._respond('BAD', tag, message) |
|---|
| 870 |
|
|---|
| 871 |
def sendPositiveResponse(self, tag = None, message = ''): |
|---|
| 872 |
self._respond('OK', tag, message) |
|---|
| 873 |
|
|---|
| 874 |
def sendNegativeResponse(self, tag = None, message = ''): |
|---|
| 875 |
self._respond('NO', tag, message) |
|---|
| 876 |
|
|---|
| 877 |
def sendUntaggedResponse(self, message, async=False): |
|---|
| 878 |
if not async or (self.blocked is None): |
|---|
| 879 |
self._respond(message, None, None) |
|---|
| 880 |
else: |
|---|
| 881 |
self._queuedAsync.append(message) |
|---|
| 882 |
|
|---|
| 883 |
def sendContinuationRequest(self, msg = 'Ready for additional command text'): |
|---|
| 884 |
if msg: |
|---|
| 885 |
self.sendLine('+ ' + msg) |
|---|
| 886 |
else: |
|---|
| 887 |
self.sendLine('+') |
|---|
| 888 |
|
|---|
| 889 |
def _respond(self, state, tag, message): |
|---|
| 890 |
if state in ('OK', 'NO', 'BAD') and self._queuedAsync: |
|---|
| 891 |
lines = self._queuedAsync |
|---|
| 892 |
self._queuedAsync = [] |
|---|
| 893 |
for msg in lines: |
|---|
| 894 |
self._respond(msg, None, None) |
|---|
| 895 |
if not tag: |
|---|
| 896 |
tag = '*' |
|---|
| 897 |
if message: |
|---|
| 898 |
self.sendLine(' '.join((tag, state, message))) |
|---|
| 899 |
else: |
|---|
| 900 |
self.sendLine(' '.join((tag, state))) |
|---|
| 901 |
|
|---|
| 902 |
def listCapabilities(self): |
|---|
| 903 |
caps = ['IMAP4rev1'] |
|---|
| 904 |
for c, v in self.capabilities().iteritems(): |
|---|
| 905 |
if v is None: |
|---|
| 906 |
caps.append(c) |
|---|
| 907 |
elif len(v): |
|---|
| 908 |
caps.extend([('%s=%s' % (c, cap)) for cap in v]) |
|---|
| 909 |
return caps |
|---|
| 910 |
|
|---|
| 911 |
def do_CAPABILITY(self, tag): |
|---|
| 912 |
self.sendUntaggedResponse('CAPABILITY ' + ' '.join(self.listCapabilities())) |
|---|
| 913 |
self.sendPositiveResponse(tag, 'CAPABILITY completed') |
|---|
| 914 |
|
|---|
| 915 |
unauth_CAPABILITY = (do_CAPABILITY,) |
|---|
| 916 |
auth_CAPABILITY = unauth_CAPABILITY |
|---|
| 917 |
select_CAPABILITY = unauth_CAPABILITY |
|---|
| 918 |
logout_CAPABILITY = unauth_CAPABILITY |
|---|
| 919 |
|
|---|
| 920 |
def do_LOGOUT(self, tag): |
|---|
| 921 |
self.sendUntaggedResponse('BYE Nice talking to you') |
|---|
| 922 |
self.sendPositiveResponse(tag, 'LOGOUT successful') |
|---|
| 923 |
self.transport.loseConnection() |
|---|
| 924 |
|
|---|
| 925 |
unauth_LOGOUT = (do_LOGOUT,) |
|---|
| 926 |
auth_LOGOUT = unauth_LOGOUT |
|---|
| 927 |
select_LOGOUT = unauth_LOGOUT |
|---|
| 928 |
logout_LOGOUT = unauth_LOGOUT |
|---|
| 929 |
|
|---|
| 930 |
def do_NOOP(self, tag): |
|---|
| 931 |
self.sendPositiveResponse(tag, 'NOOP No operation performed') |
|---|
| 932 |
|
|---|
| 933 |
unauth_NOOP = (do_NOOP,) |
|---|
| 934 |
auth_NOOP = unauth_NOOP |
|---|
| 935 |
select_NOOP = unauth_NOOP |
|---|
| 936 |
logout_NOOP = unauth_NOOP |
|---|
| 937 |
|
|---|
| 938 |
def do_AUTHENTICATE(self, tag, args): |
|---|
| 939 |
args = args.upper().strip() |
|---|
| 940 |
if args not in self.challengers: |
|---|
| 941 |
self.sendNegativeResponse(tag, 'AUTHENTICATE method unsupported') |
|---|
| 942 |
else: |
|---|
| 943 |
self.authenticate(self.challengers[args](), tag) |
|---|
| 944 |
|
|---|
| 945 |
unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom) |
|---|
| 946 |
|
|---|
| 947 |
def authenticate(self, chal, tag): |
|---|
| 948 |
if self.portal is None: |
|---|
| 949 |
self.sendNegativeResponse(tag, 'Temporary authentication failure') |
|---|
| 950 |
return |
|---|
| 951 |
|
|---|
| 952 |
self._setupChallenge(chal, tag) |
|---|
| 953 |
|
|---|
| 954 |
def _setupChallenge(self, chal, tag): |
|---|
| 955 |
try: |
|---|
| 956 |
challenge = chal.getChallenge() |
|---|
| 957 |
except Exception, e: |
|---|
| 958 |
self.sendBadResponse(tag, 'Server error: ' + str(e)) |
|---|
| 959 |
else: |
|---|
| 960 |
coded = base64.encodestring(challenge)[:-1] |
|---|
| 961 |
self.parseState = 'pending' |
|---|
| 962 |
self._pendingLiteral = defer.Deferred() |
|---|
| 963 |
self.sendContinuationRequest(coded) |
|---|
| 964 |
self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag) |
|---|
| 965 |
self._pendingLiteral.addErrback(self.__ebAuthChunk, tag) |
|---|
| 966 |
|
|---|
| 967 |
def __cbAuthChunk(self, result, chal, tag): |
|---|
| 968 |
try: |
|---|
| 969 |
uncoded = base64.decodestring(result) |
|---|
| 970 |
except binascii.Error: |
|---|
| 971 |
raise IllegalClientResponse("Malformed Response - not base64") |
|---|
| 972 |
|
|---|
| 973 |
chal.setResponse(uncoded) |
|---|
| 974 |
if chal.moreChallenges(): |
|---|
| 975 |
self._setupChallenge(chal, tag) |
|---|
| 976 |
else: |
|---|
| 977 |
self.portal.login(chal, None, IAccount).addCallbacks( |
|---|
| 978 |
self.__cbAuthResp, |
|---|
| 979 |
self.__ebAuthResp, |
|---|
| 980 |
(tag,), None, (tag,), None |
|---|
| 981 |
) |
|---|
| 982 |
|
|---|
| 983 |
def __cbAuthResp(self, (iface, avatar, logout), tag): |
|---|
| 984 |
assert iface is IAccount, "IAccount is the only supported interface" |
|---|
| 985 |
self.account = avatar |
|---|
| 986 |
self.state = 'auth' |
|---|
| 987 |
self._onLogout = logout |
|---|
| 988 |
self.sendPositiveResponse(tag, 'Authentication successful') |
|---|
| 989 |
self.setTimeout(self.POSTAUTH_TIMEOUT) |
|---|
| 990 |
|
|---|
| 991 |
def __ebAuthResp(self, failure, tag): |
|---|
| 992 |
if failure.check(cred.error.UnauthorizedLogin): |
|---|
| 993 |
self.sendNegativeResponse(tag, 'Authentication failed: unauthorized') |
|---|
| 994 |
elif failure.check(cred.error.UnhandledCredentials): |
|---|
| 995 |
self.sendNegativeResponse(tag, 'Authentication failed: server misconfigured') |
|---|
| 996 |
else: |
|---|
| 997 |
self.sendBadResponse(tag, 'Server error: login failed unexpectedly') |
|---|
| 998 |
log.err(failure) |
|---|
| 999 |
|
|---|
| 1000 |
def __ebAuthChunk(self, failure, tag): |
|---|
| 1001 |
self.sendNegativeResponse(tag, 'Authentication failed: ' + str(failure.value)) |
|---|
| 1002 |
|
|---|
| 1003 |
def do_STARTTLS(self, tag): |
|---|
| 1004 |
if self.startedTLS: |
|---|
| 1005 |
self.sendNegativeResponse(tag, 'TLS already negotiated') |
|---|
| 1006 |
elif self.ctx and self.canStartTLS: |
|---|
| 1007 |
self.sendPositiveResponse(tag, 'Begin TLS negotiation now') |
|---|
| 1008 |
self.transport.startTLS(self.ctx) |
|---|
| 1009 |
self.startedTLS = True |
|---|
| 1010 |
self.challengers = self.challengers.copy() |
|---|
| 1011 |
if 'LOGIN' not in self.challengers: |
|---|
| 1012 |
self.challengers['LOGIN'] = LOGINCredentials |
|---|
| 1013 |
if 'PLAIN' not in self.challengers: |
|---|
| 1014 |
self.challengers['PLAIN'] = PLAINCredentials |
|---|
| 1015 |
else: |
|---|
| 1016 |
self.sendNegativeResponse(tag, 'TLS not available') |
|---|
| 1017 |
|
|---|
| 1018 |
unauth_STARTTLS = (do_STARTTLS,) |
|---|
| 1019 |
|
|---|
| 1020 |
def do_LOGIN(self, tag, user, passwd): |
|---|
| 1021 |
if 'LOGINDISABLED' in self.capabilities(): |
|---|
| 1022 |
self.sendBadResponse(tag, 'LOGIN is disabled before STARTTLS') |
|---|
| 1023 |
return |
|---|
| 1024 |
|
|---|
| 1025 |
maybeDeferred(self.authenticateLogin, user, passwd |
|---|
| 1026 |
).addCallback(self.__cbLogin, tag |
|---|
| 1027 |
).addErrback(self.__ebLogin, tag |
|---|
| 1028 |
) |
|---|
| 1029 |
|
|---|
| 1030 |
unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring) |
|---|
| 1031 |
|
|---|
| 1032 |
def authenticateLogin(self, user, passwd): |
|---|
| 1033 |
"""Lookup the account associated with the given parameters |
|---|
| 1034 |
|
|---|
| 1035 |
Override this method to define the desired authentication behavior. |
|---|
| 1036 |
|
|---|
| 1037 |
The default behavior is to defer authentication to C{self.portal} |
|---|
| 1038 |
if it is not None, or to deny the login otherwise. |
|---|
| 1039 |
|
|---|
| 1040 |
@type user: C{str} |
|---|
| 1041 |
@param user: The username to lookup |
|---|
| 1042 |
|
|---|
| 1043 |
@type passwd: C{str} |
|---|
| 1044 |
@param passwd: The password to login with |
|---|
| 1045 |
""" |
|---|
| 1046 |
if self.portal: |
|---|
| 1047 |
return self.portal.login( |
|---|
| 1048 |
cred.credentials.UsernamePassword(user, passwd), |
|---|
| 1049 |
None, IAccount |
|---|
| 1050 |
) |
|---|
| 1051 |
raise cred.error.UnauthorizedLogin() |
|---|
| 1052 |
|
|---|
| 1053 |
def __cbLogin(self, (iface, avatar, logout), tag): |
|---|
| 1054 |
if iface is not IAccount: |
|---|
| 1055 |
self.sendBadResponse(tag, 'Server error: login returned unexpected value') |
|---|
| 1056 |
log.err("__cbLogin called with %r, IAccount expected" % (iface,)) |
|---|
| 1057 |
else: |
|---|
| 1058 |
self.account = avatar |
|---|
| 1059 |
self._onLogout = logout |
|---|
| 1060 |
self.sendPositiveResponse(tag, 'LOGIN succeeded') |
|---|
| 1061 |
self.state = 'auth' |
|---|
| 1062 |
self.setTimeout(self.POSTAUTH_TIMEOUT) |
|---|
| 1063 |
|
|---|
| 1064 |
def __ebLogin(self, failure, tag): |
|---|
| 1065 |
if failure.check(cred.error.UnauthorizedLogin): |
|---|
| 1066 |
self.sendNegativeResponse(tag, 'LOGIN failed') |
|---|
| 1067 |
else: |
|---|
| 1068 |
self.sendBadResponse(tag, 'Server error: ' + str(failure.value)) |
|---|
| 1069 |
log.err(failure) |
|---|
| 1070 |
|
|---|
| 1071 |
def do_NAMESPACE(self, tag): |
|---|
| 1072 |
personal = public = shared = None |
|---|
| 1073 |
np = INamespacePresenter(self.account, None) |
|---|
| 1074 |
if np is not None: |
|---|
| 1075 |
personal = np.getPersonalNamespaces() |
|---|
| 1076 |
public = np.getSharedNamespaces() |
|---|
| 1077 |
shared = np.getSharedNamespaces() |
|---|
| 1078 |
self.sendUntaggedResponse('NAMESPACE ' + collapseNestedLists([personal, public, shared])) |
|---|
| 1079 |
self.sendPositiveResponse(tag, "NAMESPACE command completed") |
|---|
| 1080 |
|
|---|
| 1081 |
auth_NAMESPACE = (do_NAMESPACE,) |
|---|
| 1082 |
select_NAMESPACE = auth_NAMESPACE |
|---|
| 1083 |
|
|---|
| 1084 |
def _parseMbox(self, name): |
|---|
| 1085 |
if isinstance(name, unicode): |
|---|
| 1086 |
return name |
|---|
| 1087 |
try: |
|---|
| 1088 |
return name.decode('imap4-utf-7') |
|---|
| 1089 |
except: |
|---|
| 1090 |
log.err() |
|---|
| 1091 |
raise IllegalMailboxEncoding(name) |
|---|
| 1092 |
|
|---|
| 1093 |
def _selectWork(self, tag, name, rw, cmdName): |
|---|
| 1094 |
if self.mbox: |
|---|
| 1095 |
self.mbox.removeListener(self) |
|---|
| 1096 |
cmbx = ICloseableMailbox(self.mbox, None) |
|---|
| 1097 |
if cmbx is not None: |
|---|
| 1098 |
maybeDeferred(cmbx.close).addErrback(log.err) |
|---|
| 1099 |
self.mbox = None |
|---|
| 1100 |
self.state = 'auth' |
|---|
| 1101 |
|
|---|
| 1102 |
name = self._parseMbox(name) |
|---|
| 1103 |
maybeDeferred(self.account.select, self._parseMbox(name), rw |
|---|
| 1104 |
).addCallback(self._cbSelectWork, cmdName, tag |
|---|
| 1105 |
).addErrback(self._ebSelectWork, cmdName, tag |
|---|
| 1106 |
) |
|---|
| 1107 |
|
|---|
| 1108 |
def _ebSelectWork(self, failure, cmdName, tag): |
|---|
| 1109 |
self.sendBadResponse(tag, "%s failed: Server error" % (cmdName,)) |
|---|
| 1110 |
log.err(failure) |
|---|
| 1111 |
|
|---|
| 1112 |
def _cbSelectWork(self, mbox, cmdName, tag): |
|---|
| 1113 |
if mbox is None: |
|---|
| 1114 |
self.sendNegativeResponse(tag, 'No such mailbox') |
|---|
| 1115 |
return |
|---|
| 1116 |
if '\\noselect' in [s.lower() for s in mbox.getFlags()]: |
|---|
| 1117 |
self.sendNegativeResponse(tag, 'Mailbox cannot be selected') |
|---|
| 1118 |
return |
|---|
| 1119 |
|
|---|
| 1120 |
flags = mbox.getFlags() |
|---|
| 1121 |
self.sendUntaggedResponse(str(mbox.getMessageCount()) + ' EXISTS') |
|---|
| 1122 |
self.sendUntaggedResponse(str(mbox.getRecentCount()) + ' RECENT') |
|---|
| 1123 |
self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags)) |
|---|
| 1124 |
self.sendPositiveResponse(None, '[UIDVALIDITY %d]' % mbox.getUIDValidity()) |
|---|
| 1125 |
|
|---|
| 1126 |
s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY' |
|---|
| 1127 |
mbox.addListener(self) |
|---|
| 1128 |
self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName)) |
|---|
| 1129 |
self.state = 'select' |
|---|
| 1130 |
self.mbox = mbox |
|---|
| 1131 |
|
|---|
| 1132 |
auth_SELECT = ( _selectWork, arg_astring, 1, 'SELECT' ) |
|---|
| 1133 |
select_SELECT = auth_SELECT |
|---|
| 1134 |
|
|---|
| 1135 |
auth_EXAMINE = ( _selectWork, arg_astring, 0, 'EXAMINE' ) |
|---|
| 1136 |
select_EXAMINE = auth_EXAMINE |
|---|
| 1137 |
|
|---|
| 1138 |
|
|---|
| 1139 |
def do_IDLE(self, tag): |
|---|
| 1140 |
self.sendContinuationRequest(None) |
|---|
| 1141 |
self.parseTag = tag |
|---|
| 1142 |
self.lastState = self.parseState |
|---|
| 1143 |
self.parseState = 'idle' |
|---|
| 1144 |
|
|---|
| 1145 |
def parse_idle(self, *args): |
|---|
| 1146 |
self.parseState = self.lastState |
|---|
| 1147 |
del self.lastState |
|---|
| 1148 |
self.sendPositiveResponse(self.parseTag, "IDLE terminated") |
|---|
| 1149 |
del self.parseTag |
|---|
| 1150 |
|
|---|
| 1151 |
select_IDLE = ( do_IDLE, ) |
|---|
| 1152 |
auth_IDLE = select_IDLE |
|---|
| 1153 |
|
|---|
| 1154 |
|
|---|
| 1155 |
def do_CREATE(self, tag, name): |
|---|
| 1156 |
name = self._parseMbox(name) |
|---|
| 1157 |
try: |
|---|
| 1158 |
result = self.account.create(name) |
|---|
| 1159 |
except MailboxException, c: |
|---|
| 1160 |
self.sendNegativeResponse(tag, str(c)) |
|---|
| 1161 |
except: |
|---|
| 1162 |
self.sendBadResponse(tag, "Server error encountered while creating mailbox") |
|---|
| 1163 |
log.err() |
|---|
| 1164 |
else: |
|---|
| 1165 |
if result: |
|---|
| 1166 |
self.sendPositiveResponse(tag, 'Mailbox created') |
|---|
| 1167 |
else: |
|---|
| 1168 |
self.sendNegativeResponse(tag, 'Mailbox not created') |
|---|
| 1169 |
|
|---|
| 1170 |
auth_CREATE = (do_CREATE, arg_astring) |
|---|
| 1171 |
select_CREATE = auth_CREATE |
|---|
| 1172 |
|
|---|
| 1173 |
def do_DELETE(self, tag, name): |
|---|
| 1174 |
name = self._parseMbox(name) |
|---|
| 1175 |
if name.lower() == 'inbox': |
|---|
| 1176 |
self.sendNegativeResponse(tag, 'You cannot delete the inbox') |
|---|
| 1177 |
return |
|---|
| 1178 |
try: |
|---|
| 1179 |
self.account.delete(name) |
|---|
| 1180 |
except MailboxException, m: |
|---|
| 1181 |
self.sendNegativeResponse(tag, str(m)) |
|---|
| 1182 |
except: |
|---|
| 1183 |
self.sendBadResponse(tag, "Server error encountered while deleting mailbox") |
|---|
| 1184 |
log.err() |
|---|
| 1185 |
else: |
|---|
| 1186 |
self.sendPositiveResponse(tag, 'Mailbox deleted') |
|---|
| 1187 |
|
|---|
| 1188 |
auth_DELETE = (do_DELETE, arg_astring) |
|---|
| 1189 |
select_DELETE = auth_DELETE |
|---|
| 1190 |
|
|---|
| 1191 |
def do_RENAME(self, tag, oldname, newname): |
|---|
| 1192 |
oldname, newname = [self._parseMbox(n) for n in oldname, newname] |
|---|
| 1193 |
if oldname.lower() == 'inbox' or newname.lower() == 'inbox': |
|---|
| 1194 |
self.sendNegativeResponse(tag, 'You cannot rename the inbox, or rename another mailbox to inbox.') |
|---|
| 1195 |
return |
|---|
| 1196 |
try: |
|---|
| 1197 |
self.account.rename(oldname, newname) |
|---|
| 1198 |
except TypeError: |
|---|
| 1199 |
self.sendBadResponse(tag, 'Invalid command syntax') |
|---|
| 1200 |
except MailboxException, m: |
|---|
| 1201 |
self.sendNegativeResponse(tag, str(m)) |
|---|
| 1202 |
except: |
|---|
| 1203 |
self.sendBadResponse(tag, "Server error encountered while renaming mailbox") |
|---|
| 1204 |
log.err() |
|---|
| 1205 |
else: |
|---|
| 1206 |
self.sendPositiveResponse(tag, 'Mailbox renamed') |
|---|
| 1207 |
|
|---|
| 1208 |
auth_RENAME = (do_RENAME, arg_astring, arg_astring) |
|---|
| 1209 |
select_RENAME = auth_RENAME |
|---|
| 1210 |
|
|---|
| 1211 |
def do_SUBSCRIBE(self, tag, name): |
|---|
| 1212 |
name = self._parseMbox(name) |
|---|
| 1213 |
try: |
|---|
| 1214 |
self.account.subscribe(name) |
|---|
| 1215 |
except MailboxException, m: |
|---|
| 1216 |
self.sendNegativeResponse(tag, str(m)) |
|---|
| 1217 |
except: |
|---|
| 1218 |
self.sendBadResponse(tag, "Server error encountered while subscribing to mailbox") |
|---|
| 1219 |
log.err() |
|---|
| 1220 |
else: |
|---|
| 1221 |
self.sendPositiveResponse(tag, 'Subscribed') |
|---|
| 1222 |
|
|---|
| 1223 |
auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring) |
|---|
| 1224 |
select_SUBSCRIBE = auth_SUBSCRIBE |
|---|
| 1225 |
|
|---|
| 1226 |
def do_UNSUBSCRIBE(self, tag, name): |
|---|
| 1227 |
name = self._parseMbox(name) |
|---|
| 1228 |
try: |
|---|
| 1229 |
self.account.unsubscribe(name) |
|---|
| 1230 |
except MailboxException, m: |
|---|
| 1231 |
self.sendNegativeResponse(tag, str(m)) |
|---|
| 1232 |
except: |
|---|
| 1233 |
self.sendBadResponse(tag, "Server error encountered while unsubscribing from mailbox") |
|---|
| 1234 |
log.err() |
|---|
| 1235 |
else: |
|---|
| 1236 |
self.sendPositiveResponse(tag, 'Unsubscribed') |
|---|
| 1237 |
|
|---|
| 1238 |
auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring) |
|---|
| 1239 |
select_UNSUBSCRIBE = auth_UNSUBSCRIBE |
|---|
| 1240 |
|
|---|
| 1241 |
def _listWork(self, tag, ref, mbox, sub, cmdName): |
|---|
| 1242 |
mbox = self._parseMbox(mbox) |
|---|
| 1243 |
maybeDeferred(self.account.listMailboxes, ref, mbox |
|---|
| 1244 |
).addCallback(self._cbListWork, tag, sub, cmdName |
|---|
| 1245 |
).addErrback(self._ebListWork, tag |
|---|
| 1246 |
) |
|---|
| 1247 |
|
|---|
| 1248 |
def _cbListWork(self, mailboxes, tag, sub, cmdName): |
|---|
| 1249 |
for (name, box) in mailboxes: |
|---|
| 1250 |
if not sub or self.account.isSubscribed(name): |
|---|
| 1251 |
flags = box.getFlags() |
|---|
| 1252 |
delim = box.getHierarchicalDelimiter() |
|---|
| 1253 |
resp = (DontQuoteMe(cmdName), map(DontQuoteMe, flags), delim, name.encode('imap4-utf-7')) |
|---|
| 1254 |
self.sendUntaggedResponse(collapseNestedLists(resp)) |
|---|
| 1255 |
self.sendPositiveResponse(tag, '%s completed' % (cmdName,)) |
|---|
| 1256 |
|
|---|
| 1257 |
def _ebListWork(self, failure, tag): |
|---|
| 1258 |
self.sendBadResponse(tag, "Server error encountered while listing mailboxes.") |
|---|
| 1259 |
log.err(failure) |
|---|
| 1260 |
|
|---|
| 1261 |
auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST') |
|---|
| 1262 |
select_LIST = auth_LIST |
|---|
| 1263 |
|
|---|
| 1264 |
auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB') |
|---|
| 1265 |
select_LSUB = auth_LSUB |
|---|
| 1266 |
|
|---|
| 1267 |
def do_STATUS(self, tag, mailbox, names): |
|---|
| 1268 |
mailbox = self._parseMbox(mailbox) |
|---|
| 1269 |
maybeDeferred(self.account.select, mailbox, 0 |
|---|
| 1270 |
).addCallback(self._cbStatusGotMailbox, tag, mailbox, names |
|---|
| 1271 |
).addErrback(self._ebStatusGotMailbox, tag |
|---|
| 1272 |
) |
|---|
| 1273 |
|
|---|
| 1274 |
def _cbStatusGotMailbox(self, mbox, tag, mailbox, names): |
|---|
| 1275 |
if mbox: |
|---|
| 1276 |
maybeDeferred(mbox.requestStatus, names).addCallbacks( |
|---|
| 1277 |
self.__cbStatus, self.__ebStatus, |
|---|
| 1278 |
(tag, mailbox), None, (tag, mailbox), None |
|---|
| 1279 |
) |
|---|
| 1280 |
else: |
|---|
| 1281 |
self.sendNegativeResponse(tag, "Could not open mailbox") |
|---|
| 1282 |
|
|---|
| 1283 |
def _ebStatusGotMailbox(self, failure, tag): |
|---|
| 1284 |
self.sendBadResponse(tag, "Server error encountered while opening mailbox.") |
|---|
| 1285 |
log.err(failure) |
|---|
| 1286 |
|
|---|
| 1287 |
auth_STATUS = (do_STATUS, arg_astring, arg_plist) |
|---|
| 1288 |
select_STATUS = auth_STATUS |
|---|
| 1289 |
|
|---|
| 1290 |
def __cbStatus(self, status, tag, box): |
|---|
| 1291 |
line = ' '.join(['%s %s' % x for x in status.iteritems()]) |
|---|
| 1292 |
self.sendUntaggedResponse('STATUS %s (%s)' % (box, line)) |
|---|
| 1293 |
self.sendPositiveResponse(tag, 'STATUS complete') |
|---|
| 1294 |
|
|---|
| 1295 |
def __ebStatus(self, failure, tag, box): |
|---|
| 1296 |
self.sendBadResponse(tag, 'STATUS %s failed: %s' % (box, str(failure.value))) |
|---|
| 1297 |
|
|---|
| 1298 |
def do_APPEND(self, tag, mailbox, flags, date, message): |
|---|
| 1299 |
mailbox = self._parseMbox(mailbox) |
|---|
| 1300 |
maybeDeferred(self.account.select, mailbox |
|---|
| 1301 |
).addCallback(self._cbAppendGotMailbox, tag, flags, date, message |
|---|
| 1302 |
).addErrback(self._ebAppendGotMailbox, tag |
|---|
| 1303 |
) |
|---|
| 1304 |
|
|---|
| 1305 |
def _cbAppendGotMailbox(self, mbox, tag, flags, date, message): |
|---|
| 1306 |
if not mbox: |
|---|
| 1307 |
self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox') |
|---|
| 1308 |
return |
|---|
| 1309 |
|
|---|
| 1310 |
d = mbox.addMessage(message, flags, date) |
|---|
| 1311 |
d.addCallback(self.__cbAppend, tag, mbox) |
|---|
| 1312 |
d.addErrback(self.__ebAppend, tag) |
|---|
| 1313 |
|
|---|
| 1314 |
def _ebAppendGotMailbox(self, failure, tag): |
|---|
| 1315 |
self.sendBadResponse(tag, "Server error encountered while opening mailbox.") |
|---|
| 1316 |
log.err(failure) |
|---|
| 1317 |
|
|---|
| 1318 |
auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime, |
|---|
| 1319 |
arg_literal) |
|---|
| 1320 |
select_APPEND = auth_APPEND |
|---|
| 1321 |
|
|---|
| 1322 |
def __cbAppend(self, result, tag, mbox): |
|---|
| 1323 |
self.sendUntaggedResponse('%d EXISTS' % mbox.getMessageCount()) |
|---|
| 1324 |
self.sendPositiveResponse(tag, 'APPEND complete') |
|---|
| 1325 |
|
|---|
| 1326 |
def __ebAppend(self, failure, tag): |
|---|
| 1327 |
self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value)) |
|---|
| 1328 |
|
|---|
| 1329 |
def do_CHECK(self, tag): |
|---|
| 1330 |
d = self.checkpoint() |
|---|
| 1331 |
if d is None: |
|---|
| 1332 |
self.__cbCheck(None, tag) |
|---|
| 1333 |
else: |
|---|
| 1334 |
d.addCallbacks( |
|---|
| 1335 |
self.__cbCheck, |
|---|
| 1336 |
self.__ebCheck, |
|---|
| 1337 |
callbackArgs=(tag,), |
|---|
| 1338 |
errbackArgs=(tag,) |
|---|
| 1339 |
) |
|---|
| 1340 |
select_CHECK = (do_CHECK,) |
|---|
| 1341 |
|
|---|
| 1342 |
def __cbCheck(self, result, tag): |
|---|
| 1343 |
self.sendPositiveResponse(tag, 'CHECK completed') |
|---|
| 1344 |
|
|---|
| 1345 |
def __ebCheck(self, failure, tag): |
|---|
| 1346 |
self.sendBadResponse(tag, 'CHECK failed: ' + str(failure.value)) |
|---|
| 1347 |
|
|---|
| 1348 |
def checkpoint(self): |
|---|
| 1349 |
"""Called when the client issues a CHECK command. |
|---|
| 1350 |
|
|---|
| 1351 |
This should perform any checkpoint operations required by the server. |
|---|
| 1352 |
It may be a long running operation, but may not block. If it returns |
|---|
| 1353 |
a deferred, the client will only be informed of success (or failure) |
|---|
| 1354 |
when the deferred's callback (or errback) is invoked. |
|---|
| 1355 |
""" |
|---|
| 1356 |
return None |
|---|
| 1357 |
|
|---|
| 1358 |
def do_CLOSE(self, tag): |
|---|
| 1359 |
d = None |
|---|
| 1360 |
if self.mbox.isWriteable(): |
|---|
| 1361 |
d = maybeDeferred(self.mbox.expunge) |
|---|
| 1362 |
cmbx = ICloseableMailbox(self.mbox, None) |
|---|
| 1363 |
if cmbx is not None: |
|---|
| 1364 |
if d is not None: |
|---|
| 1365 |
d.addCallback(lambda result: cmbx.close()) |
|---|
| 1366 |
else: |
|---|
| 1367 |
d = maybeDeferred(cmbx.close) |
|---|
| 1368 |
if d is not None: |
|---|
| 1369 |
d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,), None) |
|---|
| 1370 |
else: |
|---|
| 1371 |
self.__cbClose(None, tag) |
|---|
| 1372 |
|
|---|
| 1373 |
select_CLOSE = (do_CLOSE,) |
|---|
| 1374 |
|
|---|
| 1375 |
def __cbClose(self, result, tag): |
|---|
| 1376 |
self.sendPositiveResponse(tag, 'CLOSE completed') |
|---|
| 1377 |
self.mbox.removeListener(self) |
|---|
| 1378 |
self.mbox = None |
|---|
| 1379 |
self.state = 'auth' |
|---|
| 1380 |
|
|---|
| 1381 |
def __ebClose(self, failure, tag): |
|---|
| 1382 |
self.sendBadResponse(tag, 'CLOSE failed: ' + str(failure.value)) |
|---|
| 1383 |
|
|---|
| 1384 |
def do_EXPUNGE(self, tag): |
|---|
| 1385 |
if self.mbox.isWriteable(): |
|---|
| 1386 |
maybeDeferred(self.mbox.expunge).addCallbacks( |
|---|
| 1387 |
self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None |
|---|
| 1388 |
) |
|---|
| 1389 |
else: |
|---|
| 1390 |
self.sendNegativeResponse(tag, 'EXPUNGE ignored on read-only mailbox') |
|---|
| 1391 |
|
|---|
| 1392 |
select_EXPUNGE = (do_EXPUNGE,) |
|---|
| 1393 |
|
|---|
| 1394 |
def __cbExpunge(self, result, tag): |
|---|
| 1395 |
for e in result: |
|---|
| 1396 |
self.sendUntaggedResponse('%d EXPUNGE' % e) |
|---|
| 1397 |
self.sendPositiveResponse(tag, 'EXPUNGE completed') |
|---|
| 1398 |
|
|---|
| 1399 |
def __ebExpunge(self, failure, tag): |
|---|
| 1400 |
self.sendBadResponse(tag, 'EXPUNGE failed: ' + str(failure.value)) |
|---|
| 1401 |
log.err(failure) |
|---|
| 1402 |
|
|---|
| 1403 |
def do_SEARCH(self, tag, charset, query, uid=0): |
|---|
| 1404 |
sm = ISearchableMailbox(self.mbox, None) |
|---|
| 1405 |
if sm is not None: |
|---|
| 1406 |
maybeDeferred(sm.search, query, uid=uid).addCallbacks( |
|---|
| 1407 |
self.__cbSearch, self.__ebSearch, |
|---|
| 1408 |
(tag, self.mbox, uid), None, (tag,), None |
|---|
| 1409 |
) |
|---|
| 1410 |
else: |
|---|
| 1411 |
s = parseIdList('1:*') |
|---|
| 1412 |
maybeDeferred(self.mbox.fetch, s, uid=uid).addCallbacks( |
|---|
| 1413 |
self.__cbManualSearch, self.__ebSearch, |
|---|
| 1414 |
(tag, self.mbox, query, uid), None, (tag,), None |
|---|
| 1415 |
) |
|---|
| 1416 |
|
|---|
| 1417 |
select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys) |
|---|
| 1418 |
|
|---|
| 1419 |
def __cbSearch(self, result, tag, mbox, uid): |
|---|
| 1420 |
if uid: |
|---|
| 1421 |
result = map(mbox.getUID, result) |
|---|
| 1422 |
ids = ' '.join([str(i) for i in result]) |
|---|
| 1423 |
self.sendUntaggedResponse('SEARCH ' + ids) |
|---|
| 1424 |
self.sendPositiveResponse(tag, 'SEARCH completed') |
|---|
| 1425 |
|
|---|
| 1426 |
def __cbManualSearch(self, result, tag, mbox, query, uid, searchResults = None): |
|---|
| 1427 |
if searchResults is None: |
|---|
| 1428 |
searchResults = [] |
|---|
| 1429 |
i = 0 |
|---|
| 1430 |
for (i, (id, msg)) in zip(range(5), result): |
|---|
| 1431 |
if self.searchFilter(query, id, msg): |
|---|
| 1432 |
if uid: |
|---|
| 1433 |
searchResults.append(str(msg.getUID())) |
|---|
| 1434 |
else: |
|---|
| 1435 |
searchResults.append(str(id)) |
|---|
| 1436 |
if i == 4: |
|---|
| 1437 |
from twisted.internet import reactor |
|---|
| 1438 |
reactor.callLater(0, self.__cbManualSearch, result, tag, mbox, query, uid, searchResults) |
|---|
| 1439 |
else: |
|---|
| 1440 |
if searchResults: |
|---|
| 1441 |
self.sendUntaggedResponse('SEARCH ' + ' '.join(searchResults)) |
|---|
| 1442 |
self.sendPositiveResponse(tag, 'SEARCH completed') |
|---|
| 1443 |
|
|---|
| 1444 |
def searchFilter(self, query, id, msg): |
|---|
| 1445 |
while query: |
|---|
| 1446 |
if not self.singleSearchStep(query, id, msg): |
|---|
| 1447 |
return False |
|---|
| 1448 |
return True |
|---|
| 1449 |
|
|---|
| 1450 |
def singleSearchStep(self, query, id, msg): |
|---|
| 1451 |
q = query.pop(0) |
|---|
| 1452 |
if isinstance(q, list): |
|---|
| 1453 |
if not self.searchFilter(q, id, msg): |
|---|
| 1454 |
return False |
|---|
| 1455 |
else: |
|---|
| 1456 |
c = q.upper() |
|---|
| 1457 |
f = getattr(self, 'search_' + c) |
|---|
| 1458 |
if f: |
|---|
| 1459 |
if not f(query, id, msg): |
|---|
| 1460 |
return False |
|---|
| 1461 |
else: |
|---|
| 1462 |
|
|---|
| 1463 |
|
|---|
| 1464 |
|
|---|
| 1465 |
try: |
|---|
| 1466 |
m = parseIdList(c) |
|---|
| 1467 |
except: |
|---|
| 1468 |
log.err('Unknown search term: ' + c) |
|---|
| 1469 |
else: |
|---|
| 1470 |
if id not in m: |
|---|
| 1471 |
return False |
|---|
| 1472 |
return True |
|---|
| 1473 |
|
|---|
| 1474 |
def search_ALL(self, query, id, msg): |
|---|
| 1475 |
return True |
|---|
| 1476 |
|
|---|
| 1477 |
def search_ANSWERED(self, query, id, msg): |
|---|
| 1478 |
return '\\Answered' in msg.getFlags() |
|---|
| 1479 |
|
|---|
| 1480 |
def search_BCC(self, query, id, msg): |
|---|
| 1481 |
bcc = msg.getHeaders(False, 'bcc').get('bcc', '') |
|---|
| 1482 |
return bcc.lower().find(query.pop(0).lower()) != -1 |
|---|
| 1483 |
|
|---|
| 1484 |
def search_BEFORE(self, query, id, msg): |
|---|
| 1485 |
date = parseTime(query.pop(0)) |
|---|
| 1486 |
return rfc822.parsedate(msg.getInternalDate()) < date |
|---|
| 1487 |
|
|---|
| 1488 |
def search_BODY(self, query, id, msg): |
|---|
| 1489 |
body = query.pop(0).lower() |
|---|
| 1490 |
return text.strFile(body, msg.getBodyFile(), False) |
|---|
| 1491 |
|
|---|
| 1492 |
def search_CC(self, query, id, msg): |
|---|
| 1493 |
cc = msg.getHeaders(False, 'cc').get('cc', '') |
|---|
| 1494 |
return cc.lower().find(query.pop(0).lower()) != -1 |
|---|
| 1495 |
|
|---|
| 1496 |
def search_DELETED(self, query, id, msg): |
|---|
| 1497 |
return '\\Deleted' in msg.getFlags() |
|---|
| 1498 |
|
|---|
| 1499 |
def search_DRAFT(self, query, id, msg): |
|---|
| 1500 |
return '\\Draft' in msg.getFlags() |
|---|
| 1501 |
|
|---|
| 1502 |
def search_FLAGGED(self, query, id, msg): |
|---|
| 1503 |
return '\\Flagged' in msg.getFlags() |
|---|
| 1504 |
|
|---|
| 1505 |
def search_FROM(self, query, id, msg): |
|---|
| 1506 |
fm = msg.getHeaders(False, 'from').get('from', '') |
|---|
| 1507 |
return fm.lower().find(query.pop(0).lower()) != -1 |
|---|
| 1508 |
|
|---|
| 1509 |
def search_HEADER(self, query, id, msg): |
|---|
| 1510 |
hdr = query.pop(0).lower() |
|---|
| 1511 |
hdr = msg.getHeaders(False, hdr).get(hdr, '') |
|---|
| 1512 |
return hdr.lower().find(query.pop(0).lower()) != -1 |
|---|
| 1513 |
|
|---|
| 1514 |
def search_KEYWORD(self, query, id, msg): |
|---|
| 1515 |
query.pop(0) |
|---|
| 1516 |
return False |
|---|
| 1517 |
|
|---|
| 1518 |
def search_LARGER(self, query, id, msg): |
|---|
| 1519 |
return int(query.pop(0)) < msg.getSize() |
|---|
| 1520 |
|
|---|
| 1521 |
def search_NEW(self, query, id, msg): |
|---|
| 1522 |
return '\\Recent' in msg.getFlags() and '\\Seen' not in msg.getFlags() |
|---|
| 1523 |
|
|---|
| 1524 |
def search_NOT(self, query, id, msg): |
|---|
| 1525 |
return not self.singleSearchStep(query, id, msg) |
|---|
| 1526 |
|
|---|
| 1527 |
def search_OLD(self, query, id, msg): |
|---|
| 1528 |
return '\\Recent' not in msg.getFlags() |
|---|
| 1529 |
|
|---|
| 1530 |
def search_ON(self, query, id, msg): |
|---|
| 1531 |
date = parseTime(query.pop(0)) |
|---|
| 1532 |
return rfc822.parsedate(msg.getInternalDate()) == date |
|---|
| 1533 |
|
|---|
| 1534 |
def search_OR(self, query, id, msg): |
|---|
| 1535 |
a = self.singleSearchStep(query, id, msg) |
|---|
| 1536 |
b = self.singleSearchStep(query, id, msg) |
|---|
| 1537 |
return a or b |
|---|
| 1538 |
|
|---|
| 1539 |
def search_RECENT(self, query, id, msg): |
|---|
| 1540 |
return '\\Recent' in msg.getFlags() |
|---|
| 1541 |
|
|---|
| 1542 |
def search_SEEN(self, query, id, msg): |
|---|
| 1543 |
return '\\Seen' in msg.getFlags() |
|---|
| 1544 |
|
|---|
| 1545 |
def search_SENTBEFORE(self, query, id, msg): |
|---|
| 1546 |
date = msg.getHeader(False, 'date').get('date', '') |
|---|
| 1547 |
date = rfc822.parsedate(date) |
|---|
| 1548 |
return date < parseTime(query.pop(0)) |
|---|
| 1549 |
|
|---|
| 1550 |
def search_SENTON(self, query, id, msg): |
|---|
| 1551 |
date = msg.getHeader(False, 'date').get('date', '') |
|---|
| 1552 |
date = rfc822.parsedate(date) |
|---|
| 1553 |
return date[:3] == parseTime(query.pop(0))[:3] |
|---|
| 1554 |
|
|---|
| 1555 |
def search_SENTSINCE(self, query, id, msg): |
|---|
| 1556 |
date = msg.getHeader(False, 'date').get('date', '') |
|---|
| 1557 |
date = rfc822.parsedate(date) |
|---|
| 1558 |
return date > parseTime(query.pop(0)) |
|---|
| 1559 |
|
|---|
| 1560 |
def search_SINCE(self, query, id, msg): |
|---|
| 1561 |
date = parseTime(query.pop(0)) |
|---|
| 1562 |
return rfc822.parsedate(msg.getInternalDate()) > date |
|---|
| 1563 |
|
|---|
| 1564 |
def search_SMALLER(self, query, id, msg): |
|---|
| 1565 |
return int(query.pop(0)) > msg.getSize() |
|---|
| 1566 |
|
|---|
| 1567 |
def search_SUBJECT(self, query, id, msg): |
|---|
| 1568 |
subj = msg.getHeaders(False, 'subject').get('subject', '') |
|---|
| 1569 |
return subj.lower().find(query.pop(0).lower()) != -1 |
|---|
| 1570 |
|
|---|
| 1571 |
def search_TEXT(self, query, id, msg): |
|---|
| 1572 |
|
|---|
| 1573 |
body = query.pop(0).lower() |
|---|
| 1574 |
return text.strFile(body, msg.getBodyFile(), False) |
|---|
| 1575 |
|
|---|
| 1576 |
def search_TO(self, query, id, msg): |
|---|
| 1577 |
to = msg.getHeaders(False, 'to').get('to', '') |
|---|
| 1578 |
return to.lower().find(query.pop(0).lower()) != -1 |
|---|
| 1579 |
|
|---|
| 1580 |
def search_UID(self, query, id, msg): |
|---|
| 1581 |
c = query.pop(0) |
|---|
| 1582 |
m = parseIdList(c) |
|---|
| 1583 |
return msg.getUID() in m |
|---|
| 1584 |
|
|---|
| 1585 |
def search_UNANSWERED(self, query, id, msg): |
|---|
| 1586 |
return '\\Answered' not in msg.getFlags() |
|---|
| 1587 |
|
|---|
| 1588 |
def search_UNDELETED(self, query, id, msg): |
|---|
| 1589 |
return '\\Deleted' not in msg.getFlags() |
|---|
| 1590 |
|
|---|
| 1591 |
def search_UNDRAFT(self, query, id, msg): |
|---|
| 1592 |
return '\\Draft' not in msg.getFlags() |
|---|
| 1593 |
|
|---|
| 1594 |
def search_UNFLAGGED(self, query, id, msg): |
|---|
| 1595 |
return '\\Flagged' not in msg.getFlags() |
|---|
| 1596 |
|
|---|
| 1597 |
def search_UNKEYWORD(self, query, id, msg): |
|---|
| 1598 |
query.pop(0) |
|---|
| 1599 |
return False |
|---|
| 1600 |
|
|---|
| 1601 |
def search_UNSEEN(self, query, id, msg): |
|---|
| 1602 |
return '\\Seen' not in msg.getFlags() |
|---|
| 1603 |
|
|---|
| 1604 |
def __ebSearch(self, failure, tag): |
|---|
| 1605 |
self.sendBadResponse(tag, 'SEARCH failed: ' + str(failure.value)) |
|---|
| 1606 |
log.err(failure) |
|---|
| 1607 |
|
|---|
| 1608 |
def do_FETCH(self, tag, messages, query, uid=0): |
|---|
| 1609 |
if query: |
|---|
| 1610 |
self._oldTimeout = self.setTimeout(None) |
|---|
| 1611 |
maybeDeferred(self.mbox.fetch, messages, uid=uid |
|---|
| 1612 |
).addCallback(iter |
|---|
| 1613 |
).addCallback(self.__cbFetch, tag, query, uid |
|---|
| 1614 |
).addErrback(self.__ebFetch, tag |
|---|
| 1615 |
) |
|---|
| 1616 |
else: |
|---|
| 1617 |
self.sendPositiveResponse(tag, 'FETCH complete') |
|---|
| 1618 |
|
|---|
| 1619 |
select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt) |
|---|
| 1620 |
|
|---|
| 1621 |
def __cbFetch(self, results, tag, query, uid): |
|---|
| 1622 |
if self.blocked is None: |
|---|
| 1623 |
self.blocked = [] |
|---|
| 1624 |
try: |
|---|
| 1625 |
id, msg = results.next() |
|---|
| 1626 |
except StopIteration: |
|---|
| 1627 |
|
|---|
| 1628 |
|
|---|
| 1629 |
self.setTimeout(self._oldTimeout) |
|---|
| 1630 |
del self._oldTimeout |
|---|
| 1631 |
|
|---|
| 1632 |
|
|---|
| 1633 |
|
|---|
| 1634 |
|
|---|
| 1635 |
|
|---|
| 1636 |
|
|---|
| 1637 |
|
|---|
| 1638 |
|
|---|
| 1639 |
|
|---|
| 1640 |
|
|---|
| 1641 |
|
|---|
| 1642 |
|
|---|
| 1643 |
|
|---|
| 1644 |
self.sendPositiveResponse(tag, 'FETCH completed') |
|---|
| 1645 |
|
|---|
| 1646 |
|
|---|
| 1647 |
|
|---|
| 1648 |
|
|---|
| 1649 |
self._unblock() |
|---|
| 1650 |
else: |
|---|
| 1651 |
self.spewMessage(id, msg, query, uid |
|---|
| 1652 |
).addCallback(lambda _: self.__cbFetch(results, tag, query, uid) |
|---|
| 1653 |
).addErrback(self.__ebSpewMessage |
|---|
| 1654 |
) |
|---|
| 1655 |
|
|---|
| 1656 |
def __ebSpewMessage(self, failure): |
|---|
| 1657 |
|
|---|
| 1658 |
|
|---|
| 1659 |
|
|---|
| 1660 |
|
|---|
| 1661 |
log.err(failure) |
|---|
| 1662 |
self.transport.loseConnection() |
|---|
| 1663 |
|
|---|
| 1664 |
def spew_envelope(self, id, msg, _w=None, _f=None): |
|---|
| 1665 |
if _w is None: |
|---|
| 1666 |
_w = self.transport.write |
|---|
| 1667 |
_w('ENVELOPE ' + collapseNestedLists([getEnvelope(msg)])) |
|---|
| 1668 |
|
|---|
| 1669 |
def spew_flags(self, id, msg, _w=None, _f=None): |
|---|
| 1670 |
if _w is None: |
|---|
| 1671 |
_w = self.transport.write |
|---|
| 1672 |
_w('FLAGS ' + '(%s)' % (' '.join(msg.getFlags()))) |
|---|
| 1673 |
|
|---|
| 1674 |
def spew_internaldate(self, id, msg, _w=None, _f=None): |
|---|
| 1675 |
if _w is None: |
|---|
| 1676 |
_w = self.transport.write |
|---|
| 1677 |
idate = msg.getInternalDate() |
|---|
| 1678 |
ttup = rfc822.parsedate_tz(idate) |
|---|
| 1679 |
if ttup is None: |
|---|
| 1680 |
log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate)) |
|---|
| 1681 |
raise IMAP4Exception("Internal failure generating INTERNALDATE") |
|---|
| 1682 |
|
|---|
| 1683 |
odate = time.strftime("%d-%b-%Y %H:%M:%S ", ttup[:9]) |
|---|
| 1684 |
if ttup[9] is None: |
|---|
| 1685 |
odate = odate + "+0000" |
|---|
| 1686 |
else: |
|---|
| 1687 |
if ttup[9] >= 0: |
|---|
| 1688 |
sign = "+" |
|---|
| 1689 |
else: |
|---|
| 1690 |
sign = "-" |
|---|
| 1691 |
odate = odate + sign + string.zfill(str(((abs(ttup[9]) / 3600) * 100 + (abs(ttup[9]) % 3600) / 60)), 4) |
|---|
| 1692 |
_w('INTERNALDATE ' + _quote(odate)) |
|---|
| 1693 |
|
|---|
| 1694 |
def spew_rfc822header(self, id, msg, _w=None, _f=None): |
|---|
| 1695 |
if _w is None: |
|---|
| 1696 |
_w = self.transport.write |
|---|
| 1697 |
hdrs = _formatHeaders(msg.getHeaders(True)) |
|---|
| 1698 |
_w('RFC822.HEADER ' + _literal(hdrs)) |
|---|
| 1699 |
|
|---|
| 1700 |
def spew_rfc822text(self, id, msg, _w=None, _f=None): |
|---|
| 1701 |
if _w is None: |
|---|
| 1702 |
_w = self.transport.write |
|---|
| 1703 |
_w('RFC822.TEXT ') |
|---|
| 1704 |
_f() |
|---|
| 1705 |
return FileProducer(msg.getBodyFile() |
|---|
| 1706 |
).beginProducing(self.transport |
|---|
| 1707 |
) |
|---|
| 1708 |
|
|---|
| 1709 |
def spew_rfc822size(self, id, msg, _w=None, _f=None): |
|---|
| 1710 |
if _w is None: |
|---|
| 1711 |
_w = self.transport.write |
|---|
| 1712 |
_w('RFC822.SIZE ' + str(msg.getSize())) |
|---|
| 1713 |
|
|---|
| 1714 |
def spew_rfc822(self, id, msg, _w=None, _f=None): |
|---|
| 1715 |
if _w is None: |
|---|
| 1716 |
_w = self.transport.write |
|---|
| 1717 |
_w('RFC822 ') |
|---|
| 1718 |
_f() |
|---|
| 1719 |
mf = IMessageFile(msg, None) |
|---|
| 1720 |
if mf is not None: |
|---|
| 1721 |
return FileProducer(mf.open() |
|---|
| 1722 |
).beginProducing(self.transport |
|---|
| 1723 |
) |
|---|
| 1724 |
return MessageProducer(msg, None, self._scheduler |
|---|
| 1725 |
).beginProducing(self.transport |
|---|
| 1726 |
) |
|---|
| 1727 |
|
|---|
| 1728 |
def spew_uid(self, id, msg, _w=None, _f=None): |
|---|
| 1729 |
if _w is None: |
|---|
| 1730 |
_w = self.transport.write |
|---|
| 1731 |
_w('UID ' + str(msg.getUID())) |
|---|
| 1732 |
|
|---|
| 1733 |
def spew_bodystructure(self, id, msg, _w=None, _f=None): |
|---|
| 1734 |
_w('BODYSTRUCTURE ' + collapseNestedLists([getBodyStructure(msg, True)])) |
|---|
| 1735 |
|
|---|
| 1736 |
def spew_body(self, part, id, msg, _w=None, _f=None): |
|---|
| 1737 |
if _w is None: |
|---|
| 1738 |
_w = self.transport.write |
|---|
| 1739 |
for p in part.part: |
|---|
| 1740 |
if msg.isMultipart(): |
|---|
| 1741 |
msg = msg.getSubPart(p) |
|---|
| 1742 |
elif p > 0: |
|---|
| 1743 |
|
|---|
| 1744 |
|
|---|
| 1745 |
raise TypeError("Requested subpart of non-multipart message") |
|---|
| 1746 |
|
|---|
| 1747 |
if part.header: |
|---|
| 1748 |
hdrs = msg.getHeaders(part.header.negate, *part.header.fields) |
|---|
| 1749 |
hdrs = _formatHeaders(hdrs) |
|---|
| 1750 |
_w(str(part) + ' ' + _literal(hdrs)) |
|---|
| 1751 |
elif part.text: |
|---|
| 1752 |
_w(str(part) + ' ') |
|---|
| 1753 |
_f() |
|---|
| 1754 |
return FileProducer(msg.getBodyFile() |
|---|
| 1755 |
).beginProducing(self.transport |
|---|
| 1756 |
) |
|---|
| 1757 |
elif part.mime: |
|---|
| 1758 |
hdrs = _formatHeaders(msg.getHeaders(True)) |
|---|
| 1759 |
_w(str(part) + ' ' + _literal(hdrs)) |
|---|
| 1760 |
elif part.empty: |
|---|
| 1761 |
_w(str(part) + ' ') |
|---|
| 1762 |
_f() |
|---|
| 1763 |
if part.part: |
|---|
| 1764 |
return FileProducer(msg.getBodyFile() |
|---|
| 1765 |
).beginProducing(self.transport |
|---|
| 1766 |
) |
|---|
| 1767 |
else: |
|---|
| 1768 |
mf = IMessageFile(msg, None) |
|---|
| 1769 |
if mf is not None: |
|---|
| 1770 |
return FileProducer(mf.open()).beginProducing(self.transport) |
|---|
| 1771 |
return MessageProducer(msg, None, self._scheduler).beginProducing(self.transport) |
|---|
| 1772 |
|
|---|
| 1773 |
else: |
|---|
| 1774 |
_w('BODY ' + collapseNestedLists([getBodyStructure(msg)])) |
|---|
| 1775 |
|
|---|
| 1776 |
def spewMessage(self, id, msg, query, uid): |
|---|
| 1777 |
wbuf = WriteBuffer(self.transport) |
|---|
| 1778 |
write = wbuf.write |
|---|
| 1779 |
flush = wbuf.flush |
|---|
| 1780 |
def start(): |
|---|
| 1781 |
write('* %d FETCH (' % (id,)) |
|---|
| 1782 |
def finish(): |
|---|
| 1783 |
write(')\r\n') |
|---|
| 1784 |
def space(): |
|---|
| 1785 |
write(' ') |
|---|
| 1786 |
|
|---|
| 1787 |
def spew(): |
|---|
| 1788 |
seenUID = False |
|---|
| 1789 |
start() |
|---|
| 1790 |
for part in query: |
|---|
| 1791 |
if part.type == 'uid': |
|---|
| 1792 |
seenUID = True |
|---|
| 1793 |
if part.type == 'body': |
|---|
| 1794 |
yield self.spew_body(part, id, msg, write, flush) |
|---|
| 1795 |
else: |
|---|
| 1796 |
f = getattr(self, 'spew_' + part.type) |
|---|
| 1797 |
yield f(id, msg, write, flush) |
|---|
| 1798 |
if part is not query[-1]: |
|---|
| 1799 |
space() |
|---|
| 1800 |
if uid and not seenUID: |
|---|
| 1801 |
space() |
|---|
| 1802 |
yield self.spew_uid(id, msg, write, flush) |
|---|
| 1803 |
finish() |
|---|
| 1804 |
flush() |
|---|
| 1805 |
return self._scheduler(spew()) |
|---|
| 1806 |
|
|---|
| 1807 |
def __ebFetch(self, failure, tag): |
|---|
| 1808 |
self.setTimeout(self._oldTimeout) |
|---|
| 1809 |
del self._oldTimeout |
|---|
| 1810 |
log.err(failure) |
|---|
| 1811 |
self.sendBadResponse(tag, 'FETCH failed: ' + str(failure.value)) |
|---|
| 1812 |
|
|---|
| 1813 |
def do_STORE(self, tag, messages, mode, flags, uid=0): |
|---|
| 1814 |
mode = mode.upper() |
|---|
| 1815 |
silent = mode.endswith('SILENT') |
|---|
| 1816 |
if mode.startswith('+'): |
|---|
| 1817 |
mode = 1 |
|---|
| 1818 |
elif mode.startswith('-'): |
|---|
| 1819 |
mode = -1 |
|---|
| 1820 |
else: |
|---|
| 1821 |
mode = 0 |
|---|
| 1822 |
|
|---|
| 1823 |
maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallbacks( |
|---|
| 1824 |
self.__cbStore, self.__ebStore, (tag, self.mbox, uid, silent), None, (tag,), None |
|---|
| 1825 |
) |
|---|
| 1826 |
|
|---|
| 1827 |
select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist) |
|---|
| 1828 |
|
|---|
| 1829 |
def __cbStore(self, result, tag, mbox, uid, silent): |
|---|
| 1830 |
if result and not silent: |
|---|
| 1831 |
for (k, v) in result.iteritems(): |
|---|
| 1832 |
if uid: |
|---|
| 1833 |
uidstr = ' UID %d' % mbox.getUID(k) |
|---|
| 1834 |
else: |
|---|
| 1835 |
uidstr = '' |
|---|
| 1836 |
self.sendUntaggedResponse('%d FETCH (FLAGS (%s)%s)' % |
|---|
| 1837 |
(k, ' '.join(v), uidstr)) |
|---|
| 1838 |
self.sendPositiveResponse(tag, 'STORE completed') |
|---|
| 1839 |
|
|---|
| 1840 |
def __ebStore(self, failure, tag): |
|---|
| 1841 |
self.sendBadResponse(tag, 'Server error: ' + str(failure.value)) |
|---|
| 1842 |
|
|---|
| 1843 |
def do_COPY(self, tag, messages, mailbox, uid=0): |
|---|
| 1844 |
mailbox = self._parseMbox(mailbox) |
|---|
| 1845 |
maybeDeferred(self.account.select, mailbox |
|---|
| 1846 |
).addCallback(self._cbCopySelectedMailbox, tag, messages, mailbox, uid |
|---|
| 1847 |
).addErrback(self._ebCopySelectedMailbox, tag |
|---|
| 1848 |
) |
|---|
| 1849 |
select_COPY = (do_COPY, arg_seqset, arg_astring) |
|---|
| 1850 |
|
|---|
| 1851 |
def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid): |
|---|
| 1852 |
if not mbox: |
|---|
| 1853 |
self.sendNegativeResponse(tag, 'No such mailbox: ' + mailbox) |
|---|
| 1854 |
else: |
|---|
| 1855 |
maybeDeferred(self.mbox.fetch, messages, uid |
|---|
| 1856 |
).addCallback(self.__cbCopy, tag, mbox |
|---|
| 1857 |
).addCallback(self.__cbCopied, tag, mbox |
|---|
| 1858 |
).addErrback(self.__ebCopy, tag |
|---|
| 1859 |
) |
|---|
| 1860 |
|
|---|
| 1861 |
def _ebCopySelectedMailbox(self, failure, tag): |
|---|
| 1862 |
self.sendBadResponse(tag, 'Server error: ' + str(failure.value)) |
|---|
| 1863 |
|
|---|
| 1864 |
def __cbCopy(self, messages, tag, mbox): |
|---|
| 1865 |
|
|---|
| 1866 |
addedDeferreds = [] |
|---|
| 1867 |
addedIDs = [] |
|---|
| 1868 |
failures = [] |
|---|
| 1869 |
|
|---|
| 1870 |
fastCopyMbox = IMessageCopier(mbox, None) |
|---|
| 1871 |
for (id, msg) in messages: |
|---|
| 1872 |
if fastCopyMbox is not None: |
|---|
| 1873 |
d = maybeDeferred(fastCopyMbox.copy, msg) |
|---|
| 1874 |
addedDeferreds.append(d) |
|---|
| 1875 |
continue |
|---|
| 1876 |
|
|---|
| 1877 |
|
|---|
| 1878 |
|
|---|
| 1879 |
|
|---|
| 1880 |
flags = msg.getFlags() |
|---|
| 1881 |
date = msg.getInternalDate() |
|---|
| 1882 |
|
|---|
| 1883 |
body = IMessageFile(msg, None) |
|---|
| 1884 |
if body is not None: |
|---|
| 1885 |
bodyFile = body.open() |
|---|
| 1886 |
d = maybeDeferred(mbox.addMessage, bodyFile, flags, date) |
|---|
| 1887 |
else: |
|---|
| 1888 |
def rewind(f): |
|---|
| 1889 |
f.seek(0) |
|---|
| 1890 |
return f |
|---|
| 1891 |
buffer = tempfile.TemporaryFile() |
|---|
| 1892 |
d = MessageProducer(msg, buffer, self._scheduler |
|---|
| 1893 |
).beginProducing(None |
|---|
| 1894 |
).addCallback(lambda _, b=buffer, f=flags, d=date: mbox.addMessage(rewind(b), f, d) |
|---|
| 1895 |
) |
|---|
| 1896 |
addedDeferreds.append(d) |
|---|
| 1897 |
return defer.DeferredList(addedDeferreds) |
|---|
| 1898 |
|
|---|
| 1899 |
def __cbCopied(self, deferredIds, tag, mbox): |
|---|
| 1900 |
ids = [] |
|---|
| 1901 |
failures = [] |
|---|
| 1902 |
for (status, result) in deferredIds: |
|---|
| 1903 |
if status: |
|---|
| 1904 |
ids.append(result) |
|---|
| 1905 |
else: |
|---|
| 1906 |
failures.append(result.value) |
|---|
| 1907 |
if failures: |
|---|
| 1908 |
self.sendNegativeResponse(tag, '[ALERT] Some messages were not copied') |
|---|
| 1909 |
else: |
|---|
| 1910 |
self.sendPositiveResponse(tag, 'COPY completed') |
|---|
| 1911 |
|
|---|
| 1912 |
def __ebCopy(self, failure, tag): |
|---|
| 1913 |
self.sendBadResponse(tag, 'COPY failed:' + str(failure.value)) |
|---|
| 1914 |
log.err(failure) |
|---|
| 1915 |
|
|---|
| 1916 |
def do_UID(self, tag, command, line): |
|---|
| 1917 |
command = command.upper() |
|---|
| 1918 |
|
|---|
| 1919 |
if command not in ('COPY', 'FETCH', 'STORE', 'SEARCH'): |
|---|
| 1920 |
raise IllegalClientResponse(command) |
|---|
| 1921 |
|
|---|
| 1922 |
self.dispatchCommand(tag, command, line, uid=1) |
|---|
| 1923 |
|
|---|
| 1924 |
select_UID = (do_UID, arg_atom, arg_line) |
|---|
| 1925 |
|
|---|
| 1926 |
|
|---|
| 1927 |
|
|---|
| 1928 |
def modeChanged(self, writeable): |
|---|
| 1929 |
if writeable: |
|---|
| 1930 |
self.sendUntaggedResponse(message='[READ-WRITE]', async=True) |
|---|
| 1931 |
else: |
|---|
| 1932 |
self.sendUntaggedResponse(message='[READ-ONLY]', async=True) |
|---|
| 1933 |
|
|---|
| 1934 |
def flagsChanged(self, newFlags): |
|---|
| 1935 |
for (mId, flags) in newFlags.iteritems(): |
|---|
| 1936 |
msg = '%d FETCH (FLAGS (%s))' % (mId, ' '.join(flags)) |
|---|
| 1937 |
self.sendUntaggedResponse(msg, async=True) |
|---|
| 1938 |
|
|---|
| 1939 |
def newMessages(self, exists, recent): |
|---|
| 1940 |
if exists is not None: |
|---|
| 1941 |
self.sendUntaggedResponse('%d EXISTS' % exists, async=True) |
|---|
| 1942 |
if recent is not None: |
|---|
| 1943 |
self.sendUntaggedResponse('%d RECENT' % recent, async=True) |
|---|
| 1944 |
|
|---|
| 1945 |
|
|---|
| 1946 |
class UnhandledResponse(IMAP4Exception): pass |
|---|
| 1947 |
|
|---|
| 1948 |
class NegativeResponse(IMAP4Exception): pass |
|---|
| 1949 |
|
|---|
| 1950 |
class NoSupportedAuthentication(IMAP4Exception): |
|---|
| 1951 |
def __init__(self, serverSupports, clientSupports): |
|---|
| 1952 |
IMAP4Exception.__init__(self, 'No supported authentication schemes available') |
|---|
| 1953 |
self.serverSupports = serverSupports |
|---|
| 1954 |
self.clientSupports = clientSupports |
|---|
| 1955 |
|
|---|
| 1956 |
def __str__(self): |
|---|
| 1957 |
return (IMAP4Exception.__str__(self) |
|---|
| 1958 |
+ ': Server supports %r, client supports %r' |
|---|
| 1959 |
% (self.serverSupports, self.clientSupports)) |
|---|
| 1960 |
|
|---|
| 1961 |
class IllegalServerResponse(IMAP4Exception): pass |
|---|
| 1962 |
|
|---|
| 1963 |
TIMEOUT_ERROR = error.TimeoutError() |
|---|
| 1964 |
|
|---|
| 1965 |
class IMAP4Client(basic.LineReceiver, policies.TimeoutMixin): |
|---|
| 1966 |
"""IMAP4 client protocol implementation |
|---|
| 1967 |
|
|---|
| 1968 |
@ivar state: A string representing the state the connection is currently |
|---|
| 1969 |
in. |
|---|
| 1970 |
""" |
|---|
| 1971 |
implements(IMailboxListener) |
|---|
| 1972 |
|
|---|
| 1973 |
tags = None |
|---|
| 1974 |
waiting = None |
|---|
| 1975 |
queued = None |
|---|
| 1976 |
tagID = 1 |
|---|
| 1977 |
state = None |
|---|
| 1978 |
|
|---|
| 1979 |
startedTLS = False |
|---|
| 1980 |
|
|---|
| 1981 |
|
|---|
| 1982 |
|
|---|
| 1983 |
timeout = 0 |
|---|
| 1984 |
|
|---|
| 1985 |
|
|---|
| 1986 |
|
|---|
| 1987 |
|
|---|
| 1988 |
_capCache = None |
|---|
| 1989 |
|
|---|
| 1990 |
_memoryFileLimit = 1024 * 1024 * 10 |
|---|
| 1991 |
|
|---|
| 1992 |
|
|---|
| 1993 |
|
|---|
| 1994 |
authenticators = None |
|---|
| 1995 |
|
|---|
| 1996 |
STATUS_CODES = ('OK', 'NO', 'BAD', 'PREAUTH', 'BYE') |
|---|
| 1997 |
|
|---|
| 1998 |
STATUS_TRANSFORMATIONS = { |
|---|
| 1999 |
'MESSAGES': int, 'RECENT': int, 'UNSEEN': int |
|---|
| 2000 |
} |
|---|
| 2001 |
|
|---|
| 2002 |
context = None |
|---|
| 2003 |
|
|---|
| 2004 |
def __init__(self, contextFactory = None): |
|---|
| 2005 |
self.tags = {} |
|---|
| 2006 |
self.queued = [] |
|---|
| 2007 |
self.authenticators = {} |
|---|
| 2008 |
self.context = contextFactory |
|---|
| 2009 |
|
|---|
| 2010 |
self._tag = None |
|---|
| 2011 |
self._parts = None |
|---|
| 2012 |
self._lastCmd = None |
|---|
| 2013 |
|
|---|
| 2014 |
def registerAuthenticator(self, auth): |
|---|
| 2015 |
"""Register a new form of authentication |
|---|
| 2016 |
|
|---|
| 2017 |
When invoking the authenticate() method of IMAP4Client, the first |
|---|
| 2018 |
matching authentication scheme found will be used. The ordering is |
|---|
| 2019 |
that in which the server lists support authentication schemes. |
|---|
| 2020 |
|
|---|
| 2021 |
@type auth: Implementor of C{IClientAuthentication} |
|---|
| 2022 |
@param auth: The object to use to perform the client |
|---|
| 2023 |
side of this authentication scheme. |
|---|
| 2024 |
""" |
|---|
| 2025 |
self.authenticators[auth.getName().upper()] = auth |
|---|
| 2026 |
|
|---|
| 2027 |
def rawDataReceived(self, data): |
|---|
| 2028 |
if self.timeout > 0: |
|---|
| 2029 |
self.resetTimeout() |
|---|
| 2030 |
|
|---|
| 2031 |
self._pendingSize -= len(data) |
|---|
| 2032 |
if self._pendingSize > 0: |
|---|
| 2033 |
self._pendingBuffer.write(data) |
|---|
| 2034 |
else: |
|---|
| 2035 |
passon = '' |
|---|
| 2036 |
if self._pendingSize < 0: |
|---|
| 2037 |
data, passon = data[:self._pendingSize], data[self._pendingSize:] |
|---|
| 2038 |
self._pendingBuffer.write(data) |
|---|
| 2039 |
rest = self._pendingBuffer |
|---|
| 2040 |
self._pendingBuffer = None |
|---|
| 2041 |
self._pendingSize = None |
|---|
| 2042 |
rest.seek(0, 0) |
|---|
| 2043 |
self._parts.append(rest.read()) |
|---|
| 2044 |
self.setLineMode(passon.lstrip('\r\n')) |
|---|
| 2045 |
|
|---|
| 2046 |
|
|---|
| 2047 |
|
|---|
| 2048 |
|
|---|
| 2049 |
|
|---|
| 2050 |
def _setupForLiteral(self, rest, octets): |
|---|
| 2051 |
self._pendingBuffer = self.messageFile(octets) |
|---|
| 2052 |
self._pendingSize = octets |
|---|
| 2053 |
if self._parts is None: |
|---|
| 2054 |
self._parts = [rest, '\r\n'] |
|---|
| 2055 |
else: |
|---|
| 2056 |
self._parts.extend([rest, '\r\n']) |
|---|
| 2057 |
self.setRawMode() |
|---|
| 2058 |
|
|---|
| 2059 |
def connectionMade(self): |
|---|
| 2060 |
if self.timeout > 0: |
|---|
| 2061 |
self.setTimeout(self.timeout) |
|---|
| 2062 |
|
|---|
| 2063 |
def connectionLost(self, reason): |
|---|
| 2064 |
"""We are no longer connected""" |
|---|
| 2065 |
if self.timeout > 0: |
|---|
| 2066 |
self.setTimeout(None) |
|---|
| 2067 |
if self.queued is not None: |
|---|
| 2068 |
queued = self.queued |
|---|
| 2069 |
self.queued = None |
|---|
| 2070 |
for cmd in queued: |
|---|
| 2071 |
cmd.defer.errback(reason) |
|---|
| 2072 |
if self.tags is not None: |
|---|
| 2073 |
tags = self.tags |
|---|
| 2074 |
self.tags = None |
|---|
| 2075 |
for cmd in tags.itervalues(): |
|---|
| 2076 |
if cmd is not None and cmd.defer is not None: |
|---|
| 2077 |
cmd.defer.errback(reason) |
|---|
| 2078 |
|
|---|
| 2079 |
|
|---|
| 2080 |
def lineReceived(self, line): |
|---|
| 2081 |
""" |
|---|
| 2082 |
Attempt to parse a single line from the server. |
|---|
| 2083 |
|
|---|
| 2084 |
@type line: C{str} |
|---|
| 2085 |
@param line: The line from the server, without the line delimiter. |
|---|
| 2086 |
|
|---|
| 2087 |
@raise IllegalServerResponse: If the line or some part of the line |
|---|
| 2088 |
does not represent an allowed message from the server at this time. |
|---|
| 2089 |
""" |
|---|
| 2090 |
|
|---|
| 2091 |
if self.timeout > 0: |
|---|
| 2092 |
self.resetTimeout() |
|---|
| 2093 |
|
|---|
| 2094 |
lastPart = line.rfind('{') |
|---|
| 2095 |
if lastPart != -1: |
|---|
| 2096 |
lastPart = line[lastPart + 1:] |
|---|
| 2097 |
if lastPart.endswith('}'): |
|---|
| 2098 |
|
|---|
| 2099 |
try: |
|---|
| 2100 |
octets = int(lastPart[:-1]) |
|---|
| 2101 |
except ValueError: |
|---|
| 2102 |
raise IllegalServerResponse(line) |
|---|
| 2103 |
if self._parts is None: |
|---|
| 2104 |
self._tag, parts = line.split(None, 1) |
|---|
| 2105 |
else: |
|---|
| 2106 |
parts = line |
|---|
| 2107 |
self._setupForLiteral(parts, octets) |
|---|
| 2108 |
return |
|---|
| 2109 |
|
|---|
| 2110 |
if self._parts is None: |
|---|
| 2111 |
|
|---|
| 2112 |
self._regularDispatch(line) |
|---|
| 2113 |
else: |
|---|
| 2114 |
|
|---|
| 2115 |
|
|---|
| 2116 |
|
|---|
| 2117 |
self._parts.append(line) |
|---|
| 2118 |
tag, rest = self._tag, ''.join(self._parts) |
|---|
| 2119 |
self._tag = self._parts = None |
|---|
| 2120 |
self.dispatchCommand(tag, rest) |
|---|
| 2121 |
|
|---|
| 2122 |
def timeoutConnection(self): |
|---|
| 2123 |
if self._lastCmd and self._lastCmd.defer is not None: |
|---|
| 2124 |
d, self._lastCmd.defer = self._lastCmd.defer, None |
|---|
| 2125 |
d.errback(TIMEOUT_ERROR) |
|---|
| 2126 |
|
|---|
| 2127 |
if self.queued: |
|---|
| 2128 |
for cmd in self.queued: |
|---|
| 2129 |
if cmd.defer is not None: |
|---|
| 2130 |
d, cmd.defer = cmd.defer, d |
|---|
| 2131 |
d.errback(TIMEOUT_ERROR) |
|---|
| 2132 |
|
|---|
| 2133 |
self.transport.loseConnection() |
|---|
| 2134 |
|
|---|
| 2135 |
def _regularDispatch(self, line): |
|---|
| 2136 |
parts = line.split(None, 1) |
|---|
| 2137 |
if len(parts) != 2: |
|---|
| 2138 |
parts.append('') |
|---|
| 2139 |
tag, rest = parts |
|---|
| 2140 |
self.dispatchCommand(tag, rest) |
|---|
| 2141 |
|
|---|
| 2142 |
def messageFile(self, octets): |
|---|
| 2143 |
"""Create a file to which an incoming message may be written. |
|---|
| 2144 |
|
|---|
| 2145 |
@type octets: C{int} |
|---|
| 2146 |
@param octets: The number of octets which will be written to the file |
|---|
| 2147 |
|
|---|
| 2148 |
@rtype: Any object which implements C{write(string)} and |
|---|
| 2149 |
C{seek(int, int)} |
|---|
| 2150 |
@return: A file-like object |
|---|
| 2151 |
""" |
|---|
| 2152 |
if octets > self._memoryFileLimit: |
|---|
| 2153 |
return tempfile.TemporaryFile() |
|---|
| 2154 |
else: |
|---|
| 2155 |
return StringIO.StringIO() |
|---|
| 2156 |
|
|---|
| 2157 |
def makeTag(self): |
|---|
| 2158 |
tag = '%0.4X' % self.tagID |
|---|
| 2159 |
self.tagID += 1 |
|---|
| 2160 |
return tag |
|---|
| 2161 |
|
|---|
| 2162 |
def dispatchCommand(self, tag, rest): |
|---|
| 2163 |
if self.state is None: |
|---|
| 2164 |
f = self.response_UNAUTH |
|---|
| 2165 |
else: |
|---|
| 2166 |
f = getattr(self, 'response_' + self.state.upper(), None) |
|---|
| 2167 |
if f: |
|---|
| 2168 |
try: |
|---|
| 2169 |
f(tag, rest) |
|---|
| 2170 |
except: |
|---|
| 2171 |
log.err() |
|---|
| 2172 |
self.transport.loseConnection() |
|---|
| 2173 |
else: |
|---|
| 2174 |
log.err("Cannot dispatch: %s, %s, %s" % (self.state, tag, rest)) |
|---|
| 2175 |
self.transport.loseConnection() |
|---|
| 2176 |
|
|---|
| 2177 |
def response_UNAUTH(self, tag, rest): |
|---|
| 2178 |
if self.state is None: |
|---|
| 2179 |
|
|---|
| 2180 |
status, rest = rest.split(None, 1) |
|---|
| 2181 |
if status.upper() == 'OK': |
|---|
| 2182 |
self.state = 'unauth' |
|---|
| 2183 |
elif status.upper() == 'PREAUTH': |
|---|
| 2184 |
self.state = 'auth' |
|---|
| 2185 |
else: |
|---|
| 2186 |
|
|---|
| 2187 |
self.transport.loseConnection() |
|---|
| 2188 |
raise IllegalServerResponse(tag + ' ' + rest) |
|---|
| 2189 |
|
|---|
| 2190 |
b, e = rest.find('['), rest.find(']') |
|---|
| 2191 |
if b != -1 and e != -1: |
|---|
| 2192 |
self.serverGreeting( |
|---|
| 2193 |
self.__cbCapabilities( |
|---|
| 2194 |
([parseNestedParens(rest[b + 1:e])], None))) |
|---|
| 2195 |
else: |
|---|
| 2196 |
self.serverGreeting(None) |
|---|
| 2197 |
else: |
|---|
| 2198 |
self._defaultHandler(tag, rest) |
|---|
| 2199 |
|
|---|
| 2200 |
def response_AUTH(self, tag, rest): |
|---|
| 2201 |
self._defaultHandler(tag, rest) |
|---|
| 2202 |
|
|---|
| 2203 |
def _defaultHandler(self, tag, rest): |
|---|
| 2204 |
if tag == '*' or tag == '+': |
|---|
| 2205 |
if not self.waiting: |
|---|
| 2206 |
self._extraInfo([parseNestedParens(rest)]) |
|---|
| 2207 |
else: |
|---|
| 2208 |
cmd = self.tags[self.waiting] |
|---|
| 2209 |
if tag == '+': |
|---|
| 2210 |
cmd.continuation(rest) |
|---|
| 2211 |
else: |
|---|
| 2212 |
cmd.lines.append(rest) |
|---|
| 2213 |
else: |
|---|
| 2214 |
try: |
|---|
| 2215 |
cmd = self.tags[tag] |
|---|
| 2216 |
except KeyError: |
|---|
| 2217 |
|
|---|
| 2218 |
self.transport.loseConnection() |
|---|
| 2219 |
raise IllegalServerResponse(tag + ' ' + rest) |
|---|
| 2220 |
else: |
|---|
| 2221 |
status, line = rest.split(None, 1) |
|---|
| 2222 |
if status == 'OK': |
|---|
| 2223 |
|
|---|
| 2224 |
cmd.finish(rest, self._extraInfo) |
|---|
| 2225 |
else: |
|---|
| 2226 |
cmd.defer.errback(IMAP4Exception(line)) |
|---|
| 2227 |
del self.tags[tag] |
|---|
| 2228 |
self.waiting = None |
|---|
| 2229 |
self._flushQueue() |
|---|
| 2230 |
|
|---|
| 2231 |
def _flushQueue(self): |
|---|
| 2232 |
if self.queued: |
|---|
| 2233 |
cmd = self.queued.pop(0) |
|---|
| 2234 |
t = self.makeTag() |
|---|
| 2235 |
self.tags[t] = cmd |
|---|
| 2236 |
self.sendLine(cmd.format(t)) |
|---|
| 2237 |
self.waiting = t |
|---|
| 2238 |
|
|---|
| 2239 |
def _extraInfo(self, lines): |
|---|
| 2240 |
|
|---|
| 2241 |
|
|---|
| 2242 |
|
|---|
| 2243 |
flags = {} |
|---|
| 2244 |
recent = exists = None |
|---|
| 2245 |
for response in lines: |
|---|
| 2246 |
elements = len(response) |
|---|
| 2247 |
if elements == 1 and response[0] == ['READ-ONLY']: |
|---|
| 2248 |
self.modeChanged(False) |
|---|
| 2249 |
elif elements == 1 and response[0] == ['READ-WRITE']: |
|---|
| 2250 |
self.modeChanged(True) |
|---|
| 2251 |
elif elements == 2 and response[1] == 'EXISTS': |
|---|
| 2252 |
exists = int(response[0]) |
|---|
| 2253 |
elif elements == 2 and response[1] == 'RECENT': |
|---|
| 2254 |
recent = int(response[0]) |
|---|
| 2255 |
elif elements == 3 and response[1] == 'FETCH': |
|---|
| 2256 |
mId = int(response[0]) |
|---|
| 2257 |
values = self._parseFetchPairs(response[2]) |
|---|
| 2258 |
flags.setdefault(mId, []).extend(values.get('FLAGS', ())) |
|---|
| 2259 |
else: |
|---|
| 2260 |
log.msg('Unhandled unsolicited response: %s' % (response,)) |
|---|
| 2261 |
|
|---|
| 2262 |
if flags: |
|---|
| 2263 |
self.flagsChanged(flags) |
|---|
| 2264 |
if recent is not None or exists is not None: |
|---|
| 2265 |
self.newMessages(exists, recent) |
|---|
| 2266 |
|
|---|
| 2267 |
def sendCommand(self, cmd): |
|---|
| 2268 |
cmd.defer = defer.Deferred() |
|---|
| 2269 |
if self.waiting: |
|---|
| 2270 |
self.queued.append(cmd) |
|---|
| 2271 |
return cmd.defer |
|---|
| 2272 |
t = self.makeTag() |
|---|
| 2273 |
self.tags[t] = cmd |
|---|
| 2274 |
self.sendLine(cmd.format(t)) |
|---|
| 2275 |
self.waiting = t |
|---|
| 2276 |
self._lastCmd = cmd |
|---|
| 2277 |
return cmd.defer |
|---|
| 2278 |
|
|---|
| 2279 |
def getCapabilities(self, useCache=1): |
|---|
| 2280 |
"""Request the capabilities available on this server. |
|---|
| 2281 |
|
|---|
| 2282 |
This command is allowed in any state of connection. |
|---|
| 2283 |
|
|---|
| 2284 |
@type useCache: C{bool} |
|---|
| 2285 |
@param useCache: Specify whether to use the capability-cache or to |
|---|
| 2286 |
re-retrieve the capabilities from the server. Server capabilities |
|---|
| 2287 |
should never change, so for normal use, this flag should never be |
|---|
| 2288 |
false. |
|---|
| 2289 |
|
|---|
| 2290 |
@rtype: C{Deferred} |
|---|
| 2291 |
@return: A deferred whose callback will be invoked with a |
|---|
| 2292 |
dictionary mapping capability types to lists of supported |
|---|
| 2293 |
mechanisms, or to None if a support list is not applicable. |
|---|
| 2294 |
""" |
|---|
| 2295 |
if useCache and self._capCache is not None: |
|---|
| 2296 |
return defer.succeed(self._capCache) |
|---|
| 2297 |
cmd = 'CAPABILITY' |
|---|
| 2298 |
resp = ('CAPABILITY',) |
|---|
| 2299 |
d = self.sendCommand(Command(cmd, wantResponse=resp)) |
|---|
| 2300 |
d.addCallback(self.__cbCapabilities) |
|---|
| 2301 |
return d |
|---|
| 2302 |
|
|---|
| 2303 |
def __cbCapabilities(self, (lines, tagline)): |
|---|
| 2304 |
caps = {} |
|---|
| 2305 |
for rest in lines: |
|---|
| 2306 |
for cap in rest[1:]: |
|---|
| 2307 |
parts = cap.split('=', 1) |
|---|
| 2308 |
if len(parts) == 1: |
|---|
| 2309 |
category, value = parts[0], None |
|---|
| 2310 |
else: |
|---|
| 2311 |
category, value = parts |
|---|
| 2312 |
caps.setdefault(category, []).append(value) |
|---|
| 2313 |
|
|---|
| 2314 |
|
|---|
| 2315 |
|
|---|
| 2316 |
|
|---|
| 2317 |
for category in caps: |
|---|
| 2318 |
if caps[category] == [None]: |
|---|
| 2319 |
caps[category] = None |
|---|
| 2320 |
self._capCache = caps |
|---|
| 2321 |
return caps |
|---|
| 2322 |
|
|---|
| 2323 |
def logout(self): |
|---|
| 2324 |
"""Inform the server that we are done with the connection. |
|---|
| 2325 |
|
|---|
| 2326 |
This command is allowed in any state of connection. |
|---|
| 2327 |
|
|---|
| 2328 |
@rtype: C{Deferred} |
|---|
| 2329 |
@return: A deferred whose callback will be invoked with None |
|---|
| 2330 |
when the proper server acknowledgement has been received. |
|---|
| 2331 |
""" |
|---|
| 2332 |
d = self.sendCommand(Command('LOGOUT', wantResponse=('BYE',))) |
|---|
| 2333 |
d.addCallback(self.__cbLogout) |
|---|
| 2334 |
return d |
|---|
| 2335 |
|
|---|
| 2336 |
def __cbLogout(self, (lines, tagline)): |
|---|
| 2337 |
self.transport.loseConnection() |
|---|
| 2338 |
|
|---|
| 2339 |
return None |
|---|
| 2340 |
|
|---|
| 2341 |
|
|---|
| 2342 |
def noop(self): |
|---|
| 2343 |
"""Perform no operation. |
|---|
| 2344 |
|
|---|
| 2345 |
This command is allowed in any state of connection. |
|---|
| 2346 |
|
|---|
| 2347 |
@rtype: C{Deferred} |
|---|
| 2348 |
@return: A deferred whose callback will be invoked with a list |
|---|
| 2349 |
of untagged status updates the server responds with. |
|---|
| 2350 |
""" |
|---|
| 2351 |
d = self.sendCommand(Command('NOOP')) |
|---|
| 2352 |
d.addCallback(self.__cbNoop) |
|---|
| 2353 |
return d |
|---|
| 2354 |
|
|---|
| 2355 |
def __cbNoop(self, (lines, tagline)): |
|---|
| 2356 |
|
|---|
| 2357 |
|
|---|
| 2358 |
return lines |
|---|
| 2359 |
|
|---|
| 2360 |
def startTLS(self, contextFactory=None): |
|---|
| 2361 |
""" |
|---|
| 2362 |
Initiates a 'STARTTLS' request and negotiates the TLS / SSL |
|---|
| 2363 |
Handshake. |
|---|
| 2364 |
|
|---|
| 2365 |
@param contextFactory: The TLS / SSL Context Factory to |
|---|
| 2366 |
leverage. If the contextFactory is None the IMAP4Client will |
|---|
| 2367 |
either use the current TLS / SSL Context Factory or attempt to |
|---|
| 2368 |
create a new one. |
|---|
| 2369 |
|
|---|
| 2370 |
@type contextFactory: C{ssl.ClientContextFactory} |
|---|
| 2371 |
|
|---|
| 2372 |
@return: A Deferred which fires when the transport has been |
|---|
| 2373 |
secured according to the given contextFactory, or which fails |
|---|
| 2374 |
if the transport cannot be secured. |
|---|
| 2375 |
""" |
|---|
| 2376 |
assert not self.startedTLS, "Client and Server are currently communicating via TLS" |
|---|
| 2377 |
|
|---|
| 2378 |
if contextFactory is None: |
|---|
| 2379 |
contextFactory = self._getContextFactory() |
|---|
| 2380 |
|
|---|
| 2381 |
if contextFactory is None: |
|---|
| 2382 |
return defer.fail(IMAP4Exception( |
|---|
| 2383 |
"IMAP4Client requires a TLS context to " |
|---|
| 2384 |
"initiate the STARTTLS handshake")) |
|---|
| 2385 |
|
|---|
| 2386 |
if 'STARTTLS' not in self._capCache: |
|---|
| 2387 |
return defer.fail(IMAP4Exception( |
|---|
| 2388 |
"Server does not support secure communication " |
|---|
| 2389 |
"via TLS / SSL")) |
|---|
| 2390 |
|
|---|
| 2391 |
tls = interfaces.ITLSTransport(self.transport, None) |
|---|
| 2392 |
if tls is None: |
|---|
| 2393 |
return defer.fail(IMAP4Exception( |
|---|
| 2394 |
"IMAP4Client transport does not implement " |
|---|
| 2395 |
"interfaces.ITLSTransport")) |
|---|
| 2396 |
|
|---|
| 2397 |
d = self.sendCommand(Command('STARTTLS')) |
|---|
| 2398 |
d.addCallback(self._startedTLS, contextFactory) |
|---|
| 2399 |
d.addCallback(lambda _: self.getCapabilities()) |
|---|
| 2400 |
return d |
|---|
| 2401 |
|
|---|
| 2402 |
|
|---|
| 2403 |
def authenticate(self, secret): |
|---|
| 2404 |
"""Attempt to enter the authenticated state with the server |
|---|
| 2405 |
|
|---|
| 2406 |
This command is allowed in the Non-Authenticated state. |
|---|
| 2407 |
|
|---|
| 2408 |
@rtype: C{Deferred} |
|---|
| 2409 |
@return: A deferred whose callback is invoked if the authentication |
|---|
| 2410 |
succeeds and whose errback will be invoked otherwise. |
|---|
| 2411 |
""" |
|---|
| 2412 |
if self._capCache is None: |
|---|
| 2413 |
d = self.getCapabilities() |
|---|
| 2414 |
else: |
|---|
| 2415 |
d = defer.succeed(self._capCache) |
|---|
| 2416 |
d.addCallback(self.__cbAuthenticate, secret) |
|---|
| 2417 |
return d |
|---|
| 2418 |
|
|---|
| 2419 |
def __cbAuthenticate(self, caps, secret): |
|---|
| 2420 |
auths = caps.get('AUTH', ()) |
|---|
| 2421 |
for scheme in auths: |
|---|
| 2422 |
if scheme.upper() in self.authenticators: |
|---|
| 2423 |
cmd = Command('AUTHENTICATE', scheme, (), |
|---|
| 2424 |
self.__cbContinueAuth, scheme, |
|---|
| 2425 |
secret) |
|---|
| 2426 |
return self.sendCommand(cmd) |
|---|
| 2427 |
|
|---|
| 2428 |
if self.startedTLS: |
|---|
| 2429 |
return defer.fail(NoSupportedAuthentication( |
|---|
| 2430 |
auths, self.authenticators.keys())) |
|---|
| 2431 |
else: |
|---|
| 2432 |
def ebStartTLS(err): |
|---|
| 2433 |
err.trap(IMAP4Exception) |
|---|
| 2434 |
|
|---|
| 2435 |
return defer.fail(NoSupportedAuthentication( |
|---|
| 2436 |
auths, self.authenticators.keys())) |
|---|
| 2437 |
|
|---|
| 2438 |
d = self.startTLS() |
|---|
| 2439 |
d.addErrback(ebStartTLS) |
|---|
| 2440 |
d.addCallback(lambda _: self.getCapabilities()) |
|---|
| 2441 |
d.addCallback(self.__cbAuthTLS, secret) |
|---|
| 2442 |
return d |
|---|
| 2443 |
|
|---|
| 2444 |
|
|---|
| 2445 |
def __cbContinueAuth(self, rest, scheme, secret): |
|---|
| 2446 |
try: |
|---|
| 2447 |
chal = base64.decodestring(rest + '\n') |
|---|
| 2448 |
except binascii.Error: |
|---|
| 2449 |
self.sendLine('*') |
|---|
| 2450 |
raise IllegalServerResponse(rest) |
|---|
| 2451 |
self.transport.loseConnection() |
|---|
| 2452 |
else: |
|---|
| 2453 |
auth = self.authenticators[scheme] |
|---|
| 2454 |
chal = auth.challengeResponse(secret, chal) |
|---|
| 2455 |
self.sendLine(base64.encodestring(chal).strip()) |
|---|
| 2456 |
|
|---|
| 2457 |
def __cbAuthTLS(self, caps, secret): |
|---|
| 2458 |
auths = caps.get('AUTH', ()) |
|---|
| 2459 |
for scheme in auths: |
|---|
| 2460 |
if scheme.upper() in self.authenticators: |
|---|
| 2461 |
cmd = Command('AUTHENTICATE', scheme, (), |
|---|
| 2462 |
self.__cbContinueAuth, scheme, |
|---|
| 2463 |
secret) |
|---|
| 2464 |
return self.sendCommand(cmd) |
|---|
| 2465 |
raise NoSupportedAuthentication(auths, self.authenticators.keys()) |
|---|
| 2466 |
|
|---|
| 2467 |
|
|---|
| 2468 |
def login(self, username, password): |
|---|
| 2469 |
"""Authenticate with the server using a username and password |
|---|
| 2470 |
|
|---|
| 2471 |
This command is allowed in the Non-Authenticated state. If the |
|---|
| 2472 |
server supports the STARTTLS capability and our transport supports |
|---|
| 2473 |
TLS, TLS is negotiated before the login command is issued. |
|---|
| 2474 |
|
|---|
| 2475 |
A more secure way to log in is to use C{startTLS} or |
|---|
| 2476 |
C{authenticate} or both. |
|---|
| 2477 |
|
|---|
| 2478 |
@type username: C{str} |
|---|
| 2479 |
@param username: The username to log in with |
|---|
| 2480 |
|
|---|
| 2481 |
@type password: C{str} |
|---|
| 2482 |
@param password: The password to log in with |
|---|
| 2483 |
|
|---|
| 2484 |
@rtype: C{Deferred} |
|---|
| 2485 |
@return: A deferred whose callback is invoked if login is successful |
|---|
| 2486 |
and whose errback is invoked otherwise. |
|---|
| 2487 |
""" |
|---|
| 2488 |
d = maybeDeferred(self.getCapabilities) |
|---|
| 2489 |
d.addCallback(self.__cbLoginCaps, username, password) |
|---|
| 2490 |
return d |
|---|
| 2491 |
|
|---|
| 2492 |
def serverGreeting(self, caps): |
|---|
| 2493 |
"""Called when the server has sent us a greeting. |
|---|
| 2494 |
|
|---|
| 2495 |
@type caps: C{dict} |
|---|
| 2496 |
@param caps: Capabilities the server advertised in its greeting. |
|---|
| 2497 |
""" |
|---|
| 2498 |
|
|---|
| 2499 |
def _getContextFactory(self): |
|---|
| 2500 |
if self.context is not None: |
|---|
| 2501 |
return self.context |
|---|
| 2502 |
try: |
|---|
| 2503 |
from twisted.internet import ssl |
|---|
| 2504 |
except ImportError: |
|---|
| 2505 |
return None |
|---|
| 2506 |
else: |
|---|
| 2507 |
context = ssl.ClientContextFactory() |
|---|
| 2508 |
context.method = ssl.SSL.TLSv1_METHOD |
|---|
| 2509 |
return context |
|---|
| 2510 |
|
|---|
| 2511 |
def __cbLoginCaps(self, capabilities, username, password): |
|---|
| 2512 |
|
|---|
| 2513 |
tryTLS = 'STARTTLS' in capabilities |
|---|
| 2514 |
|
|---|
| 2515 |
|
|---|
| 2516 |
tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None |
|---|
| 2517 |
|
|---|
| 2518 |
|
|---|
| 2519 |
nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None |
|---|
| 2520 |
|
|---|
| 2521 |
if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport: |
|---|
| 2522 |
d = self.startTLS() |
|---|
| 2523 |
|
|---|
| 2524 |
d.addCallbacks( |
|---|
| 2525 |
self.__cbLoginTLS, |
|---|
| 2526 |
self.__ebLoginTLS, |
|---|
| 2527 |
callbackArgs=(username, password), |
|---|
| 2528 |
) |
|---|
| 2529 |
return d |
|---|
| 2530 |
else: |
|---|
| 2531 |
if nontlsTransport: |
|---|
| 2532 |
log.msg("Server has no TLS support. logging in over cleartext!") |
|---|
| 2533 |
args = ' '.join((_quote(username), _quote(password))) |
|---|
| 2534 |
return self.sendCommand(Command('LOGIN', args)) |
|---|
| 2535 |
|
|---|
| 2536 |
def _startedTLS(self, result, context): |
|---|
| 2537 |
self.transport.startTLS(context) |
|---|
| 2538 |
self._capCache = None |
|---|
| 2539 |
self.startedTLS = True |
|---|
| 2540 |
return result |
|---|
| 2541 |
|
|---|
| 2542 |
def __cbLoginTLS(self, result, username, password): |
|---|
| 2543 |
args = ' '.join((_quote(username), _quote(password))) |
|---|
| 2544 |
return self.sendCommand(Command('LOGIN', args)) |
|---|
| 2545 |
|
|---|
| 2546 |
def __ebLoginTLS(self, failure): |
|---|
| 2547 |
log.err(failure) |
|---|
| 2548 |
return failure |
|---|
| 2549 |
|
|---|
| 2550 |
def namespace(self): |
|---|
| 2551 |
"""Retrieve information about the namespaces available to this account |
|---|
| 2552 |
|
|---|
| 2553 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2554 |
|
|---|
| 2555 |
@rtype: C{Deferred} |
|---|
| 2556 |
@return: A deferred whose callback is invoked with namespace |
|---|
| 2557 |
information. An example of this information is:: |
|---|
| 2558 |
|
|---|
| 2559 |
[[['', '/']], [], []] |
|---|
| 2560 |
|
|---|
| 2561 |
which indicates a single personal namespace called '' with '/' |
|---|
| 2562 |
as its hierarchical delimiter, and no shared or user namespaces. |
|---|
| 2563 |
""" |
|---|
| 2564 |
cmd = 'NAMESPACE' |
|---|
| 2565 |
resp = ('NAMESPACE',) |
|---|
| 2566 |
d = self.sendCommand(Command(cmd, wantResponse=resp)) |
|---|
| 2567 |
d.addCallback(self.__cbNamespace) |
|---|
| 2568 |
return d |
|---|
| 2569 |
|
|---|
| 2570 |
def __cbNamespace(self, (lines, last)): |
|---|
| 2571 |
for parts in lines: |
|---|
| 2572 |
if len(parts) == 4 and parts[0] == 'NAMESPACE': |
|---|
| 2573 |
return [e or [] for e in parts[1:]] |
|---|
| 2574 |
log.err("No NAMESPACE response to NAMESPACE command") |
|---|
| 2575 |
return [[], [], []] |
|---|
| 2576 |
|
|---|
| 2577 |
def select(self, mailbox): |
|---|
| 2578 |
"""Select a mailbox |
|---|
| 2579 |
|
|---|
| 2580 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2581 |
|
|---|
| 2582 |
@type mailbox: C{str} |
|---|
| 2583 |
@param mailbox: The name of the mailbox to select |
|---|
| 2584 |
|
|---|
| 2585 |
@rtype: C{Deferred} |
|---|
| 2586 |
@return: A deferred whose callback is invoked with mailbox |
|---|
| 2587 |
information if the select is successful and whose errback is |
|---|
| 2588 |
invoked otherwise. Mailbox information consists of a dictionary |
|---|
| 2589 |
with the following keys and values:: |
|---|
| 2590 |
|
|---|
| 2591 |
FLAGS: A list of strings containing the flags settable on |
|---|
| 2592 |
messages in this mailbox. |
|---|
| 2593 |
|
|---|
| 2594 |
EXISTS: An integer indicating the number of messages in this |
|---|
| 2595 |
mailbox. |
|---|
| 2596 |
|
|---|
| 2597 |
RECENT: An integer indicating the number of \"recent\" |
|---|
| 2598 |
messages in this mailbox. |
|---|
| 2599 |
|
|---|
| 2600 |
UNSEEN: An integer indicating the number of messages not |
|---|
| 2601 |
flagged \\Seen in this mailbox. |
|---|
| 2602 |
|
|---|
| 2603 |
PERMANENTFLAGS: A list of strings containing the flags that |
|---|
| 2604 |
can be permanently set on messages in this mailbox. |
|---|
| 2605 |
|
|---|
| 2606 |
UIDVALIDITY: An integer uniquely identifying this mailbox. |
|---|
| 2607 |
""" |
|---|
| 2608 |
cmd = 'SELECT' |
|---|
| 2609 |
args = _prepareMailboxName(mailbox) |
|---|
| 2610 |
resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY') |
|---|
| 2611 |
d = self.sendCommand(Command(cmd, args, wantResponse=resp)) |
|---|
| 2612 |
d.addCallback(self.__cbSelect, 1) |
|---|
| 2613 |
return d |
|---|
| 2614 |
|
|---|
| 2615 |
def examine(self, mailbox): |
|---|
| 2616 |
"""Select a mailbox in read-only mode |
|---|
| 2617 |
|
|---|
| 2618 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2619 |
|
|---|
| 2620 |
@type mailbox: C{str} |
|---|
| 2621 |
@param mailbox: The name of the mailbox to examine |
|---|
| 2622 |
|
|---|
| 2623 |
@rtype: C{Deferred} |
|---|
| 2624 |
@return: A deferred whose callback is invoked with mailbox |
|---|
| 2625 |
information if the examine is successful and whose errback |
|---|
| 2626 |
is invoked otherwise. Mailbox information consists of a dictionary |
|---|
| 2627 |
with the following keys and values:: |
|---|
| 2628 |
|
|---|
| 2629 |
'FLAGS': A list of strings containing the flags settable on |
|---|
| 2630 |
messages in this mailbox. |
|---|
| 2631 |
|
|---|
| 2632 |
'EXISTS': An integer indicating the number of messages in this |
|---|
| 2633 |
mailbox. |
|---|
| 2634 |
|
|---|
| 2635 |
'RECENT': An integer indicating the number of \"recent\" |
|---|
| 2636 |
messages in this mailbox. |
|---|
| 2637 |
|
|---|
| 2638 |
'UNSEEN': An integer indicating the number of messages not |
|---|
| 2639 |
flagged \\Seen in this mailbox. |
|---|
| 2640 |
|
|---|
| 2641 |
'PERMANENTFLAGS': A list of strings containing the flags that |
|---|
| 2642 |
can be permanently set on messages in this mailbox. |
|---|
| 2643 |
|
|---|
| 2644 |
'UIDVALIDITY': An integer uniquely identifying this mailbox. |
|---|
| 2645 |
""" |
|---|
| 2646 |
cmd = 'EXAMINE' |
|---|
| 2647 |
args = _prepareMailboxName(mailbox) |
|---|
| 2648 |
resp = ('FLAGS', 'EXISTS', 'RECENT', 'UNSEEN', 'PERMANENTFLAGS', 'UIDVALIDITY') |
|---|
| 2649 |
d = self.sendCommand(Command(cmd, args, wantResponse=resp)) |
|---|
| 2650 |
d.addCallback(self.__cbSelect, 0) |
|---|
| 2651 |
return d |
|---|
| 2652 |
|
|---|
| 2653 |
|
|---|
| 2654 |
def _intOrRaise(self, value, phrase): |
|---|
| 2655 |
""" |
|---|
| 2656 |
Parse C{value} as an integer and return the result or raise |
|---|
| 2657 |
L{IllegalServerResponse} with C{phrase} as an argument if C{value} |
|---|
| 2658 |
cannot be parsed as an integer. |
|---|
| 2659 |
""" |
|---|
| 2660 |
try: |
|---|
| 2661 |
return int(value) |
|---|
| 2662 |
except ValueError: |
|---|
| 2663 |
raise IllegalServerResponse(phrase) |
|---|
| 2664 |
|
|---|
| 2665 |
|
|---|
| 2666 |
def __cbSelect(self, (lines, tagline), rw): |
|---|
| 2667 |
""" |
|---|
| 2668 |
Handle lines received in response to a SELECT or EXAMINE command. |
|---|
| 2669 |
|
|---|
| 2670 |
See RFC 3501, section 6.3.1. |
|---|
| 2671 |
""" |
|---|
| 2672 |
|
|---|
| 2673 |
|
|---|
| 2674 |
datum = {'READ-WRITE': rw} |
|---|
| 2675 |
lines.append(parseNestedParens(tagline)) |
|---|
| 2676 |
for split in lines: |
|---|
| 2677 |
if len(split) > 0 and split[0].upper() == 'OK': |
|---|
| 2678 |
|
|---|
| 2679 |
content = split[1] |
|---|
| 2680 |
key = content[0].upper() |
|---|
| 2681 |
if key == 'READ-ONLY': |
|---|
| 2682 |
datum['READ-WRITE'] = False |
|---|
| 2683 |
elif key == 'READ-WRITE': |
|---|
| 2684 |
datum['READ-WRITE'] = True |
|---|
| 2685 |
elif key == 'UIDVALIDITY': |
|---|
| 2686 |
datum['UIDVALIDITY'] = self._intOrRaise( |
|---|
| 2687 |
content[1], split) |
|---|
| 2688 |
elif key == 'UNSEEN': |
|---|
| 2689 |
datum['UNSEEN'] = self._intOrRaise(content[1], split) |
|---|
| 2690 |
elif key == 'UIDNEXT': |
|---|
| 2691 |
datum['UIDNEXT'] = self._intOrRaise(content[1], split) |
|---|
| 2692 |
elif key == 'PERMANENTFLAGS': |
|---|
| 2693 |
datum['PERMANENTFLAGS'] = tuple(content[1]) |
|---|
| 2694 |
else: |
|---|
| 2695 |
log.err('Unhandled SELECT response (2): %s' % (split,)) |
|---|
| 2696 |
elif len(split) == 2: |
|---|
| 2697 |
|
|---|
| 2698 |
if split[0].upper() == 'FLAGS': |
|---|
| 2699 |
datum['FLAGS'] = tuple(split[1]) |
|---|
| 2700 |
elif isinstance(split[1], str): |
|---|
| 2701 |
|
|---|
| 2702 |
|
|---|
| 2703 |
|
|---|
| 2704 |
if split[1].upper() == 'EXISTS': |
|---|
| 2705 |
datum['EXISTS'] = self._intOrRaise(split[0], split) |
|---|
| 2706 |
elif split[1].upper() == 'RECENT': |
|---|
| 2707 |
datum['RECENT'] = self._intOrRaise(split[0], split) |
|---|
| 2708 |
else: |
|---|
| 2709 |
log.err('Unhandled SELECT response (0): %s' % (split,)) |
|---|
| 2710 |
else: |
|---|
| 2711 |
log.err('Unhandled SELECT response (1): %s' % (split,)) |
|---|
| 2712 |
else: |
|---|
| 2713 |
log.err('Unhandled SELECT response (4): %s' % (split,)) |
|---|
| 2714 |
return datum |
|---|
| 2715 |
|
|---|
| 2716 |
|
|---|
| 2717 |
def create(self, name): |
|---|
| 2718 |
"""Create a new mailbox on the server |
|---|
| 2719 |
|
|---|
| 2720 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2721 |
|
|---|
| 2722 |
@type name: C{str} |
|---|
| 2723 |
@param name: The name of the mailbox to create. |
|---|
| 2724 |
|
|---|
| 2725 |
@rtype: C{Deferred} |
|---|
| 2726 |
@return: A deferred whose callback is invoked if the mailbox creation |
|---|
| 2727 |
is successful and whose errback is invoked otherwise. |
|---|
| 2728 |
""" |
|---|
| 2729 |
return self.sendCommand(Command('CREATE', _prepareMailboxName(name))) |
|---|
| 2730 |
|
|---|
| 2731 |
def delete(self, name): |
|---|
| 2732 |
"""Delete a mailbox |
|---|
| 2733 |
|
|---|
| 2734 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2735 |
|
|---|
| 2736 |
@type name: C{str} |
|---|
| 2737 |
@param name: The name of the mailbox to delete. |
|---|
| 2738 |
|
|---|
| 2739 |
@rtype: C{Deferred} |
|---|
| 2740 |
@return: A deferred whose calblack is invoked if the mailbox is |
|---|
| 2741 |
deleted successfully and whose errback is invoked otherwise. |
|---|
| 2742 |
""" |
|---|
| 2743 |
return self.sendCommand(Command('DELETE', _prepareMailboxName(name))) |
|---|
| 2744 |
|
|---|
| 2745 |
def rename(self, oldname, newname): |
|---|
| 2746 |
"""Rename a mailbox |
|---|
| 2747 |
|
|---|
| 2748 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2749 |
|
|---|
| 2750 |
@type oldname: C{str} |
|---|
| 2751 |
@param oldname: The current name of the mailbox to rename. |
|---|
| 2752 |
|
|---|
| 2753 |
@type newname: C{str} |
|---|
| 2754 |
@param newname: The new name to give the mailbox. |
|---|
| 2755 |
|
|---|
| 2756 |
@rtype: C{Deferred} |
|---|
| 2757 |
@return: A deferred whose callback is invoked if the rename is |
|---|
| 2758 |
successful and whose errback is invoked otherwise. |
|---|
| 2759 |
""" |
|---|
| 2760 |
oldname = _prepareMailboxName(oldname) |
|---|
| 2761 |
newname = _prepareMailboxName(newname) |
|---|
| 2762 |
return self.sendCommand(Command('RENAME', ' '.join((oldname, newname)))) |
|---|
| 2763 |
|
|---|
| 2764 |
def subscribe(self, name): |
|---|
| 2765 |
"""Add a mailbox to the subscription list |
|---|
| 2766 |
|
|---|
| 2767 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2768 |
|
|---|
| 2769 |
@type name: C{str} |
|---|
| 2770 |
@param name: The mailbox to mark as 'active' or 'subscribed' |
|---|
| 2771 |
|
|---|
| 2772 |
@rtype: C{Deferred} |
|---|
| 2773 |
@return: A deferred whose callback is invoked if the subscription |
|---|
| 2774 |
is successful and whose errback is invoked otherwise. |
|---|
| 2775 |
""" |
|---|
| 2776 |
return self.sendCommand(Command('SUBSCRIBE', _prepareMailboxName(name))) |
|---|
| 2777 |
|
|---|
| 2778 |
def unsubscribe(self, name): |
|---|
| 2779 |
"""Remove a mailbox from the subscription list |
|---|
| 2780 |
|
|---|
| 2781 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2782 |
|
|---|
| 2783 |
@type name: C{str} |
|---|
| 2784 |
@param name: The mailbox to unsubscribe |
|---|
| 2785 |
|
|---|
| 2786 |
@rtype: C{Deferred} |
|---|
| 2787 |
@return: A deferred whose callback is invoked if the unsubscription |
|---|
| 2788 |
is successful and whose errback is invoked otherwise. |
|---|
| 2789 |
""" |
|---|
| 2790 |
return self.sendCommand(Command('UNSUBSCRIBE', _prepareMailboxName(name))) |
|---|
| 2791 |
|
|---|
| 2792 |
def list(self, reference, wildcard): |
|---|
| 2793 |
"""List a subset of the available mailboxes |
|---|
| 2794 |
|
|---|
| 2795 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2796 |
|
|---|
| 2797 |
@type reference: C{str} |
|---|
| 2798 |
@param reference: The context in which to interpret C{wildcard} |
|---|
| 2799 |
|
|---|
| 2800 |
@type wildcard: C{str} |
|---|
| 2801 |
@param wildcard: The pattern of mailbox names to match, optionally |
|---|
| 2802 |
including either or both of the '*' and '%' wildcards. '*' will |
|---|
| 2803 |
match zero or more characters and cross hierarchical boundaries. |
|---|
| 2804 |
'%' will also match zero or more characters, but is limited to a |
|---|
| 2805 |
single hierarchical level. |
|---|
| 2806 |
|
|---|
| 2807 |
@rtype: C{Deferred} |
|---|
| 2808 |
@return: A deferred whose callback is invoked with a list of C{tuple}s, |
|---|
| 2809 |
the first element of which is a C{tuple} of mailbox flags, the second |
|---|
| 2810 |
element of which is the hierarchy delimiter for this mailbox, and the |
|---|
| 2811 |
third of which is the mailbox name; if the command is unsuccessful, |
|---|
| 2812 |
the deferred's errback is invoked instead. |
|---|
| 2813 |
""" |
|---|
| 2814 |
cmd = 'LIST' |
|---|
| 2815 |
args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7')) |
|---|
| 2816 |
resp = ('LIST',) |
|---|
| 2817 |
d = self.sendCommand(Command(cmd, args, wantResponse=resp)) |
|---|
| 2818 |
d.addCallback(self.__cbList, 'LIST') |
|---|
| 2819 |
return d |
|---|
| 2820 |
|
|---|
| 2821 |
def lsub(self, reference, wildcard): |
|---|
| 2822 |
"""List a subset of the subscribed available mailboxes |
|---|
| 2823 |
|
|---|
| 2824 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2825 |
|
|---|
| 2826 |
The parameters and returned object are the same as for the C{list} |
|---|
| 2827 |
method, with one slight difference: Only mailboxes which have been |
|---|
| 2828 |
subscribed can be included in the resulting list. |
|---|
| 2829 |
""" |
|---|
| 2830 |
cmd = 'LSUB' |
|---|
| 2831 |
args = '"%s" "%s"' % (reference, wildcard.encode('imap4-utf-7')) |
|---|
| 2832 |
resp = ('LSUB',) |
|---|
| 2833 |
d = self.sendCommand(Command(cmd, args, wantResponse=resp)) |
|---|
| 2834 |
d.addCallback(self.__cbList, 'LSUB') |
|---|
| 2835 |
return d |
|---|
| 2836 |
|
|---|
| 2837 |
def __cbList(self, (lines, last), command): |
|---|
| 2838 |
results = [] |
|---|
| 2839 |
for parts in lines: |
|---|
| 2840 |
if len(parts) == 4 and parts[0] == command: |
|---|
| 2841 |
parts[1] = tuple(parts[1]) |
|---|
| 2842 |
results.append(tuple(parts[1:])) |
|---|
| 2843 |
return results |
|---|
| 2844 |
|
|---|
| 2845 |
def status(self, mailbox, *names): |
|---|
| 2846 |
""" |
|---|
| 2847 |
Retrieve the status of the given mailbox |
|---|
| 2848 |
|
|---|
| 2849 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2850 |
|
|---|
| 2851 |
@type mailbox: C{str} |
|---|
| 2852 |
@param mailbox: The name of the mailbox to query |
|---|
| 2853 |
|
|---|
| 2854 |
@type *names: C{str} |
|---|
| 2855 |
@param *names: The status names to query. These may be any number of: |
|---|
| 2856 |
C{'MESSAGES'}, C{'RECENT'}, C{'UIDNEXT'}, C{'UIDVALIDITY'}, and |
|---|
| 2857 |
C{'UNSEEN'}. |
|---|
| 2858 |
|
|---|
| 2859 |
@rtype: C{Deferred} |
|---|
| 2860 |
@return: A deferred which fires with with the status information if the |
|---|
| 2861 |
command is successful and whose errback is invoked otherwise. The |
|---|
| 2862 |
status information is in the form of a C{dict}. Each element of |
|---|
| 2863 |
C{names} is a key in the dictionary. The value for each key is the |
|---|
| 2864 |
corresponding response from the server. |
|---|
| 2865 |
""" |
|---|
| 2866 |
cmd = 'STATUS' |
|---|
| 2867 |
args = "%s (%s)" % (_prepareMailboxName(mailbox), ' '.join(names)) |
|---|
| 2868 |
resp = ('STATUS',) |
|---|
| 2869 |
d = self.sendCommand(Command(cmd, args, wantResponse=resp)) |
|---|
| 2870 |
d.addCallback(self.__cbStatus) |
|---|
| 2871 |
return d |
|---|
| 2872 |
|
|---|
| 2873 |
def __cbStatus(self, (lines, last)): |
|---|
| 2874 |
status = {} |
|---|
| 2875 |
for parts in lines: |
|---|
| 2876 |
if parts[0] == 'STATUS': |
|---|
| 2877 |
items = parts[2] |
|---|
| 2878 |
items = [items[i:i+2] for i in range(0, len(items), 2)] |
|---|
| 2879 |
status.update(dict(items)) |
|---|
| 2880 |
for k in status.keys(): |
|---|
| 2881 |
t = self.STATUS_TRANSFORMATIONS.get(k) |
|---|
| 2882 |
if t: |
|---|
| 2883 |
try: |
|---|
| 2884 |
status[k] = t(status[k]) |
|---|
| 2885 |
except Exception, e: |
|---|
| 2886 |
raise IllegalServerResponse('(%s %s): %s' % (k, status[k], str(e))) |
|---|
| 2887 |
return status |
|---|
| 2888 |
|
|---|
| 2889 |
def append(self, mailbox, message, flags = (), date = None): |
|---|
| 2890 |
"""Add the given message to the given mailbox. |
|---|
| 2891 |
|
|---|
| 2892 |
This command is allowed in the Authenticated and Selected states. |
|---|
| 2893 |
|
|---|
| 2894 |
@type mailbox: C{str} |
|---|
| 2895 |
@param mailbox: The mailbox to which to add this message. |
|---|
| 2896 |
|
|---|
| 2897 |
@type message: Any file-like object |
|---|
| 2898 |
@param message: The message to add, in RFC822 format. Newlines |
|---|
| 2899 |
in this file should be \\r\\n-style. |
|---|
| 2900 |
|
|---|
| 2901 |
@type flags: Any iterable of C{str} |
|---|
| 2902 |
@param flags: The flags to associated with this message. |
|---|
| 2903 |
|
|---|
| 2904 |
@type date: C{str} |
|---|
| 2905 |
@param date: The date to associate with this message. This should |
|---|
| 2906 |
be of the format DD-MM-YYYY HH:MM:SS +/-HHMM. For example, in |
|---|
| 2907 |
Eastern Standard Time, on July 1st 2004 at half past 1 PM, |
|---|
| 2908 |
\"01-07-2004 13:30:00 -0500\". |
|---|
| 2909 |
|
|---|
| 2910 |
@rtype: C{Deferred} |
|---|
| 2911 |
@return: A deferred whose callback is invoked when this command |
|---|
| 2912 |
succeeds or whose errback is invoked if it fails. |
|---|
| 2913 |
""" |
|---|
| 2914 |
message.seek(0, 2) |
|---|
| 2915 |
L = message.tell() |
|---|
| 2916 |
message.seek(0, 0) |
|---|
| 2917 |
fmt = '%s (%s)%s {%d}' |
|---|
| 2918 |
if date: |
|---|
| 2919 |
date = ' "%s"' % date |
|---|
| 2920 |
else: |
|---|
| 2921 |
date = '' |
|---|
| 2922 |
cmd = fmt % ( |
|---|
| 2923 |
_prepareMailboxName(mailbox), ' '.join(flags), |
|---|
| 2924 |
date, L |
|---|
| 2925 |
) |
|---|
| 2926 |
d = self.sendCommand(Command('APPEND', cmd, (), self.__cbContinueAppend, message)) |
|---|
| 2927 |
return d |
|---|
| 2928 |
|
|---|
| 2929 |
def __cbContinueAppend(self, lines, message): |
|---|
| 2930 |
s = basic.FileSender() |
|---|
| 2931 |
return s.beginFileTransfer(message, self.transport, None |
|---|
| 2932 |
).addCallback(self.__cbFinishAppend) |
|---|
| 2933 |
|
|---|
| 2934 |
def __cbFinishAppend(self, foo): |
|---|
| 2935 |
self.sendLine('') |
|---|
| 2936 |
|
|---|
| 2937 |
def check(self): |
|---|
| 2938 |
"""Tell the server to perform a checkpoint |
|---|
| 2939 |
|
|---|
| 2940 |
This command is allowed in the Selected state. |
|---|
| 2941 |
|
|---|
| 2942 |
@rtype: C{Deferred} |
|---|
| 2943 |
@return: A deferred whose callback is invoked when this command |
|---|
| 2944 |
succeeds or whose errback is invoked if it fails. |
|---|
| 2945 |
""" |
|---|
| 2946 |
return self.sendCommand(Command('CHECK')) |
|---|
| 2947 |
|
|---|
| 2948 |
def close(self): |
|---|
| 2949 |
"""Return the connection to the Authenticated state. |
|---|
| 2950 |
|
|---|
| 2951 |
This command is allowed in the Selected state. |
|---|
| 2952 |
|
|---|
| 2953 |
Issuing this command will also remove all messages flagged \\Deleted |
|---|
| 2954 |
from the selected mailbox if it is opened in read-write mode, |
|---|
| 2955 |
otherwise it indicates success by no messages are removed. |
|---|
| 2956 |
|
|---|
| 2957 |
@rtype: C{Deferred} |
|---|
| 2958 |
@return: A deferred whose callback is invoked when the command |
|---|
| 2959 |
completes successfully or whose errback is invoked if it fails. |
|---|
| 2960 |
""" |
|---|
| 2961 |
return self.sendCommand(Command('CLOSE')) |
|---|
| 2962 |
|
|---|
| 2963 |
|
|---|
| 2964 |
def expunge(self): |
|---|
| 2965 |
"""Return the connection to the Authenticate state. |
|---|
| 2966 |
|
|---|
| 2967 |
This command is allowed in the Selected state. |
|---|
| 2968 |
|
|---|
| 2969 |
Issuing this command will perform the same actions as issuing the |
|---|
| 2970 |
close command, but will also generate an 'expunge' response for |
|---|
| 2971 |
every message deleted. |
|---|
| 2972 |
|
|---|
| 2973 |
@rtype: C{Deferred} |
|---|
| 2974 |
@return: A deferred whose callback is invoked with a list of the |
|---|
| 2975 |
'expunge' responses when this command is successful or whose errback |
|---|
| 2976 |
is invoked otherwise. |
|---|
| 2977 |
""" |
|---|
| 2978 |
cmd = 'EXPUNGE' |
|---|
| 2979 |
resp = ('EXPUNGE',) |
|---|
| 2980 |
d = self.sendCommand(Command(cmd, wantResponse=resp)) |
|---|
| 2981 |
d.addCallback(self.__cbExpunge) |
|---|
| 2982 |
return d |
|---|
| 2983 |
|
|---|
| 2984 |
|
|---|
| 2985 |
def __cbExpunge(self, (lines, last)): |
|---|
| 2986 |
ids = [] |
|---|
| 2987 |
for parts in lines: |
|---|
| 2988 |
if len(parts) == 2 and parts[1] == 'EXPUNGE': |
|---|
| 2989 |
ids.append(self._intOrRaise(parts[0], parts)) |
|---|
| 2990 |
return ids |
|---|
| 2991 |
|
|---|
| 2992 |
|
|---|
| 2993 |
def search(self, *queries, **kwarg): |
|---|
| 2994 |
"""Search messages in the currently selected mailbox |
|---|
| 2995 |
|
|---|
| 2996 |
This command is allowed in the Selected state. |
|---|
| 2997 |
|
|---|
| 2998 |
Any non-zero number of queries are accepted by this method, as |
|---|
| 2999 |
returned by the C{Query}, C{Or}, and C{Not} functions. |
|---|
| 3000 |
|
|---|
| 3001 |
One keyword argument is accepted: if uid is passed in with a non-zero |
|---|
| 3002 |
value, the server is asked to return message UIDs instead of message |
|---|
| 3003 |
sequence numbers. |
|---|
| 3004 |
|
|---|
| 3005 |
@rtype: C{Deferred} |
|---|
| 3006 |
@return: A deferred whose callback will be invoked with a list of all |
|---|
| 3007 |
the message sequence numbers return by the search, or whose errback |
|---|
| 3008 |
will be invoked if there is an error. |
|---|
| 3009 |
""" |
|---|
| 3010 |
if kwarg.get('uid'): |
|---|
| 3011 |
cmd = 'UID SEARCH' |
|---|
| 3012 |
else: |
|---|
| 3013 |
cmd = 'SEARCH' |
|---|
| 3014 |
args = ' '.join(queries) |
|---|
| 3015 |
d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,))) |
|---|
| 3016 |
d.addCallback(self.__cbSearch) |
|---|
| 3017 |
return d |
|---|
| 3018 |
|
|---|
| 3019 |
|
|---|
| 3020 |
def __cbSearch(self, (lines, end)): |
|---|
| 3021 |
ids = [] |
|---|
| 3022 |
for parts in lines: |
|---|
| 3023 |
if len(parts) > 0 and parts[0] == 'SEARCH': |
|---|
| 3024 |
ids.extend([self._intOrRaise(p, parts) for p in parts[1:]]) |
|---|
| 3025 |
return ids |
|---|
| 3026 |
|
|---|
| 3027 |
|
|---|
| 3028 |
def fetchUID(self, messages, uid=0): |
|---|
| 3029 |
"""Retrieve the unique identifier for one or more messages |
|---|
| 3030 |
|
|---|
| 3031 |
This command is allowed in the Selected state. |
|---|
| 3032 |
|
|---|
| 3033 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3034 |
@param messages: A message sequence set |
|---|
| 3035 |
|
|---|
| 3036 |
@type uid: C{bool} |
|---|
| 3037 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3038 |
numbers or of unique message IDs. |
|---|
| 3039 |
|
|---|
| 3040 |
@rtype: C{Deferred} |
|---|
| 3041 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3042 |
message sequence numbers to unique message identifiers, or whose |
|---|
| 3043 |
errback is invoked if there is an error. |
|---|
| 3044 |
""" |
|---|
| 3045 |
return self._fetch(messages, useUID=uid, uid=1) |
|---|
| 3046 |
|
|---|
| 3047 |
|
|---|
| 3048 |
def fetchFlags(self, messages, uid=0): |
|---|
| 3049 |
"""Retrieve the flags for one or more messages |
|---|
| 3050 |
|
|---|
| 3051 |
This command is allowed in the Selected state. |
|---|
| 3052 |
|
|---|
| 3053 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3054 |
@param messages: The messages for which to retrieve flags. |
|---|
| 3055 |
|
|---|
| 3056 |
@type uid: C{bool} |
|---|
| 3057 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3058 |
numbers or of unique message IDs. |
|---|
| 3059 |
|
|---|
| 3060 |
@rtype: C{Deferred} |
|---|
| 3061 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3062 |
message numbers to lists of flags, or whose errback is invoked if |
|---|
| 3063 |
there is an error. |
|---|
| 3064 |
""" |
|---|
| 3065 |
return self._fetch(str(messages), useUID=uid, flags=1) |
|---|
| 3066 |
|
|---|
| 3067 |
|
|---|
| 3068 |
def fetchInternalDate(self, messages, uid=0): |
|---|
| 3069 |
"""Retrieve the internal date associated with one or more messages |
|---|
| 3070 |
|
|---|
| 3071 |
This command is allowed in the Selected state. |
|---|
| 3072 |
|
|---|
| 3073 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3074 |
@param messages: The messages for which to retrieve the internal date. |
|---|
| 3075 |
|
|---|
| 3076 |
@type uid: C{bool} |
|---|
| 3077 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3078 |
numbers or of unique message IDs. |
|---|
| 3079 |
|
|---|
| 3080 |
@rtype: C{Deferred} |
|---|
| 3081 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3082 |
message numbers to date strings, or whose errback is invoked |
|---|
| 3083 |
if there is an error. Date strings take the format of |
|---|
| 3084 |
\"day-month-year time timezone\". |
|---|
| 3085 |
""" |
|---|
| 3086 |
return self._fetch(str(messages), useUID=uid, internaldate=1) |
|---|
| 3087 |
|
|---|
| 3088 |
|
|---|
| 3089 |
def fetchEnvelope(self, messages, uid=0): |
|---|
| 3090 |
"""Retrieve the envelope data for one or more messages |
|---|
| 3091 |
|
|---|
| 3092 |
This command is allowed in the Selected state. |
|---|
| 3093 |
|
|---|
| 3094 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3095 |
@param messages: The messages for which to retrieve envelope data. |
|---|
| 3096 |
|
|---|
| 3097 |
@type uid: C{bool} |
|---|
| 3098 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3099 |
numbers or of unique message IDs. |
|---|
| 3100 |
|
|---|
| 3101 |
@rtype: C{Deferred} |
|---|
| 3102 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3103 |
message numbers to envelope data, or whose errback is invoked |
|---|
| 3104 |
if there is an error. Envelope data consists of a sequence of the |
|---|
| 3105 |
date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to, |
|---|
| 3106 |
and message-id header fields. The date, subject, in-reply-to, and |
|---|
| 3107 |
message-id fields are strings, while the from, sender, reply-to, |
|---|
| 3108 |
to, cc, and bcc fields contain address data. Address data consists |
|---|
| 3109 |
of a sequence of name, source route, mailbox name, and hostname. |
|---|
| 3110 |
Fields which are not present for a particular address may be C{None}. |
|---|
| 3111 |
""" |
|---|
| 3112 |
return self._fetch(str(messages), useUID=uid, envelope=1) |
|---|
| 3113 |
|
|---|
| 3114 |
|
|---|
| 3115 |
def fetchBodyStructure(self, messages, uid=0): |
|---|
| 3116 |
"""Retrieve the structure of the body of one or more messages |
|---|
| 3117 |
|
|---|
| 3118 |
This command is allowed in the Selected state. |
|---|
| 3119 |
|
|---|
| 3120 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3121 |
@param messages: The messages for which to retrieve body structure |
|---|
| 3122 |
data. |
|---|
| 3123 |
|
|---|
| 3124 |
@type uid: C{bool} |
|---|
| 3125 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3126 |
numbers or of unique message IDs. |
|---|
| 3127 |
|
|---|
| 3128 |
@rtype: C{Deferred} |
|---|
| 3129 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3130 |
message numbers to body structure data, or whose errback is invoked |
|---|
| 3131 |
if there is an error. Body structure data describes the MIME-IMB |
|---|
| 3132 |
format of a message and consists of a sequence of mime type, mime |
|---|
| 3133 |
subtype, parameters, content id, description, encoding, and size. |
|---|
| 3134 |
The fields following the size field are variable: if the mime |
|---|
| 3135 |
type/subtype is message/rfc822, the contained message's envelope |
|---|
| 3136 |
information, body structure data, and number of lines of text; if |
|---|
| 3137 |
the mime type is text, the number of lines of text. Extension fields |
|---|
| 3138 |
may also be included; if present, they are: the MD5 hash of the body, |
|---|
| 3139 |
body disposition, body language. |
|---|
| 3140 |
""" |
|---|
| 3141 |
return self._fetch(messages, useUID=uid, bodystructure=1) |
|---|
| 3142 |
|
|---|
| 3143 |
|
|---|
| 3144 |
def fetchSimplifiedBody(self, messages, uid=0): |
|---|
| 3145 |
"""Retrieve the simplified body structure of one or more messages |
|---|
| 3146 |
|
|---|
| 3147 |
This command is allowed in the Selected state. |
|---|
| 3148 |
|
|---|
| 3149 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3150 |
@param messages: A message sequence set |
|---|
| 3151 |
|
|---|
| 3152 |
@type uid: C{bool} |
|---|
| 3153 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3154 |
numbers or of unique message IDs. |
|---|
| 3155 |
|
|---|
| 3156 |
@rtype: C{Deferred} |
|---|
| 3157 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3158 |
message numbers to body data, or whose errback is invoked |
|---|
| 3159 |
if there is an error. The simplified body structure is the same |
|---|
| 3160 |
as the body structure, except that extension fields will never be |
|---|
| 3161 |
present. |
|---|
| 3162 |
""" |
|---|
| 3163 |
return self._fetch(messages, useUID=uid, body=1) |
|---|
| 3164 |
|
|---|
| 3165 |
|
|---|
| 3166 |
def fetchMessage(self, messages, uid=0): |
|---|
| 3167 |
"""Retrieve one or more entire messages |
|---|
| 3168 |
|
|---|
| 3169 |
This command is allowed in the Selected state. |
|---|
| 3170 |
|
|---|
| 3171 |
@type messages: L{MessageSet} or C{str} |
|---|
| 3172 |
@param messages: A message sequence set |
|---|
| 3173 |
|
|---|
| 3174 |
@type uid: C{bool} |
|---|
| 3175 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3176 |
numbers or of unique message IDs. |
|---|
| 3177 |
|
|---|
| 3178 |
@rtype: L{Deferred} |
|---|
| 3179 |
|
|---|
| 3180 |
@return: A L{Deferred} which will fire with a C{dict} mapping message |
|---|
| 3181 |
sequence numbers to C{dict}s giving message data for the |
|---|
| 3182 |
corresponding message. If C{uid} is true, the inner dictionaries |
|---|
| 3183 |
have a C{'UID'} key mapped to a C{str} giving the UID for the |
|---|
| 3184 |
message. The text of the message is a C{str} associated with the |
|---|
| 3185 |
C{'RFC822'} key in each dictionary. |
|---|
| 3186 |
""" |
|---|
| 3187 |
return self._fetch(messages, useUID=uid, rfc822=1) |
|---|
| 3188 |
|
|---|
| 3189 |
|
|---|
| 3190 |
def fetchHeaders(self, messages, uid=0): |
|---|
| 3191 |
"""Retrieve headers of one or more messages |
|---|
| 3192 |
|
|---|
| 3193 |
This command is allowed in the Selected state. |
|---|
| 3194 |
|
|---|
| 3195 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3196 |
@param messages: A message sequence set |
|---|
| 3197 |
|
|---|
| 3198 |
@type uid: C{bool} |
|---|
| 3199 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3200 |
numbers or of unique message IDs. |
|---|
| 3201 |
|
|---|
| 3202 |
@rtype: C{Deferred} |
|---|
| 3203 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3204 |
message numbers to dicts of message headers, or whose errback is |
|---|
| 3205 |
invoked if there is an error. |
|---|
| 3206 |
""" |
|---|
| 3207 |
return self._fetch(messages, useUID=uid, rfc822header=1) |
|---|
| 3208 |
|
|---|
| 3209 |
|
|---|
| 3210 |
def fetchBody(self, messages, uid=0): |
|---|
| 3211 |
"""Retrieve body text of one or more messages |
|---|
| 3212 |
|
|---|
| 3213 |
This command is allowed in the Selected state. |
|---|
| 3214 |
|
|---|
| 3215 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3216 |
@param messages: A message sequence set |
|---|
| 3217 |
|
|---|
| 3218 |
@type uid: C{bool} |
|---|
| 3219 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3220 |
numbers or of unique message IDs. |
|---|
| 3221 |
|
|---|
| 3222 |
@rtype: C{Deferred} |
|---|
| 3223 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3224 |
message numbers to file-like objects containing body text, or whose |
|---|
| 3225 |
errback is invoked if there is an error. |
|---|
| 3226 |
""" |
|---|
| 3227 |
return self._fetch(messages, useUID=uid, rfc822text=1) |
|---|
| 3228 |
|
|---|
| 3229 |
|
|---|
| 3230 |
def fetchSize(self, messages, uid=0): |
|---|
| 3231 |
"""Retrieve the size, in octets, of one or more messages |
|---|
| 3232 |
|
|---|
| 3233 |
This command is allowed in the Selected state. |
|---|
| 3234 |
|
|---|
| 3235 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3236 |
@param messages: A message sequence set |
|---|
| 3237 |
|
|---|
| 3238 |
@type uid: C{bool} |
|---|
| 3239 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3240 |
numbers or of unique message IDs. |
|---|
| 3241 |
|
|---|
| 3242 |
@rtype: C{Deferred} |
|---|
| 3243 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3244 |
message numbers to sizes, or whose errback is invoked if there is |
|---|
| 3245 |
an error. |
|---|
| 3246 |
""" |
|---|
| 3247 |
return self._fetch(messages, useUID=uid, rfc822size=1) |
|---|
| 3248 |
|
|---|
| 3249 |
|
|---|
| 3250 |
def fetchFull(self, messages, uid=0): |
|---|
| 3251 |
"""Retrieve several different fields of one or more messages |
|---|
| 3252 |
|
|---|
| 3253 |
This command is allowed in the Selected state. This is equivalent |
|---|
| 3254 |
to issuing all of the C{fetchFlags}, C{fetchInternalDate}, |
|---|
| 3255 |
C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody} |
|---|
| 3256 |
functions. |
|---|
| 3257 |
|
|---|
| 3258 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3259 |
@param messages: A message sequence set |
|---|
| 3260 |
|
|---|
| 3261 |
@type uid: C{bool} |
|---|
| 3262 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3263 |
numbers or of unique message IDs. |
|---|
| 3264 |
|
|---|
| 3265 |
@rtype: C{Deferred} |
|---|
| 3266 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3267 |
message numbers to dict of the retrieved data values, or whose |
|---|
| 3268 |
errback is invoked if there is an error. They dictionary keys |
|---|
| 3269 |
are "flags", "date", "size", "envelope", and "body". |
|---|
| 3270 |
""" |
|---|
| 3271 |
return self._fetch( |
|---|
| 3272 |
messages, useUID=uid, flags=1, internaldate=1, |
|---|
| 3273 |
rfc822size=1, envelope=1, body=1) |
|---|
| 3274 |
|
|---|
| 3275 |
|
|---|
| 3276 |
def fetchAll(self, messages, uid=0): |
|---|
| 3277 |
"""Retrieve several different fields of one or more messages |
|---|
| 3278 |
|
|---|
| 3279 |
This command is allowed in the Selected state. This is equivalent |
|---|
| 3280 |
to issuing all of the C{fetchFlags}, C{fetchInternalDate}, |
|---|
| 3281 |
C{fetchSize}, and C{fetchEnvelope} functions. |
|---|
| 3282 |
|
|---|
| 3283 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3284 |
@param messages: A message sequence set |
|---|
| 3285 |
|
|---|
| 3286 |
@type uid: C{bool} |
|---|
| 3287 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3288 |
numbers or of unique message IDs. |
|---|
| 3289 |
|
|---|
| 3290 |
@rtype: C{Deferred} |
|---|
| 3291 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3292 |
message numbers to dict of the retrieved data values, or whose |
|---|
| 3293 |
errback is invoked if there is an error. They dictionary keys |
|---|
| 3294 |
are "flags", "date", "size", and "envelope". |
|---|
| 3295 |
""" |
|---|
| 3296 |
return self._fetch( |
|---|
| 3297 |
messages, useUID=uid, flags=1, internaldate=1, |
|---|
| 3298 |
rfc822size=1, envelope=1) |
|---|
| 3299 |
|
|---|
| 3300 |
|
|---|
| 3301 |
def fetchFast(self, messages, uid=0): |
|---|
| 3302 |
"""Retrieve several different fields of one or more messages |
|---|
| 3303 |
|
|---|
| 3304 |
This command is allowed in the Selected state. This is equivalent |
|---|
| 3305 |
to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and |
|---|
| 3306 |
C{fetchSize} functions. |
|---|
| 3307 |
|
|---|
| 3308 |
@type messages: C{MessageSet} or C{str} |
|---|
| 3309 |
@param messages: A message sequence set |
|---|
| 3310 |
|
|---|
| 3311 |
@type uid: C{bool} |
|---|
| 3312 |
@param uid: Indicates whether the message sequence set is of message |
|---|
| 3313 |
numbers or of unique message IDs. |
|---|
| 3314 |
|
|---|
| 3315 |
@rtype: C{Deferred} |
|---|
| 3316 |
@return: A deferred whose callback is invoked with a dict mapping |
|---|
| 3317 |
message numbers to dict of the retrieved data values, or whose |
|---|
| 3318 |
errback is invoked if there is an error. They dictionary keys are |
|---|
| 3319 |
"flags", "date", and "size". |
|---|
| 3320 |
""" |
|---|
| 3321 |
return self._fetch( |
|---|
| 3322 |
messages, useUID=uid, flags=1, internaldate=1, rfc822size=1) |
|---|
| 3323 |
|
|---|
| 3324 |
|
|---|
| 3325 |
def _parseFetchPairs(self, fetchResponseList): |
|---|
| 3326 |
""" |
|---|
| 3327 |
Given the result of parsing a single I{FETCH} response, construct a |
|---|
| 3328 |
C{dict} mapping response keys to response values. |
|---|
| 3329 |
|
|---|
| 3330 |
@param fetchResponseList: The result of parsing a I{FETCH} response |
|---|
| 3331 |
with L{parseNestedParens} and extracting just the response data |
|---|
| 3332 |
(that is, just the part that comes after C{"FETCH"}). The form |
|---|
| 3333 |
of this input (and therefore the output of this method) is very |
|---|
| 3334 |
disagreable. A valuable improvement would be to enumerate the |
|---|
| 3335 |
possible keys (representing them as structured objects of some |
|---|
| 3336 |
sort) rather than using strings and tuples of tuples of strings |
|---|
| 3337 |
and so forth. This would allow the keys to be documented more |
|---|
| 3338 |
easily and would allow for a much simpler application-facing API |
|---|
| 3339 |
(one not based on looking up somewhat hard to predict keys in a |
|---|
| 3340 |
dict). Since C{fetchResponseList} notionally represents a |
|---|
| 3341 |
flattened sequence of pairs (identifying keys followed by their |
|---|
| 3342 |
associated values), collapsing such complex elements of this |
|---|
| 3343 |
list as C{["BODY", ["HEADER.FIELDS", ["SUBJECT"]]]} into a |
|---|
| 3344 |
single object would also greatly simplify the implementation of |
|---|
| 3345 |
this method. |
|---|
| 3346 |
|
|---|
| 3347< |
|---|