| 1 |
|
|---|
| 2 |
|
|---|
| 3 |
|
|---|
| 4 |
|
|---|
| 5 |
|
|---|
| 6 |
|
|---|
| 7 |
""" |
|---|
| 8 |
Post-office Protocol version 3 |
|---|
| 9 |
|
|---|
| 10 |
@author: Glyph Lefkowitz |
|---|
| 11 |
@author: Jp Calderone |
|---|
| 12 |
""" |
|---|
| 13 |
|
|---|
| 14 |
import string |
|---|
| 15 |
import base64 |
|---|
| 16 |
import binascii |
|---|
| 17 |
import warnings |
|---|
| 18 |
|
|---|
| 19 |
from zope.interface import implements, Interface |
|---|
| 20 |
|
|---|
| 21 |
from twisted.mail import smtp |
|---|
| 22 |
from twisted.protocols import basic |
|---|
| 23 |
from twisted.protocols import policies |
|---|
| 24 |
from twisted.internet import task |
|---|
| 25 |
from twisted.internet import defer |
|---|
| 26 |
from twisted.internet import interfaces |
|---|
| 27 |
from twisted.python import log |
|---|
| 28 |
from twisted.python.hashlib import md5 |
|---|
| 29 |
|
|---|
| 30 |
from twisted import cred |
|---|
| 31 |
import twisted.cred.error |
|---|
| 32 |
import twisted.cred.credentials |
|---|
| 33 |
|
|---|
| 34 |
|
|---|
| 35 |
|
|---|
| 36 |
|
|---|
| 37 |
class APOPCredentials: |
|---|
| 38 |
implements(cred.credentials.IUsernamePassword) |
|---|
| 39 |
|
|---|
| 40 |
def __init__(self, magic, username, digest): |
|---|
| 41 |
self.magic = magic |
|---|
| 42 |
self.username = username |
|---|
| 43 |
self.digest = digest |
|---|
| 44 |
|
|---|
| 45 |
def checkPassword(self, password): |
|---|
| 46 |
seed = self.magic + password |
|---|
| 47 |
myDigest = md5(seed).hexdigest() |
|---|
| 48 |
return myDigest == self.digest |
|---|
| 49 |
|
|---|
| 50 |
|
|---|
| 51 |
class _HeadersPlusNLines: |
|---|
| 52 |
def __init__(self, f, n): |
|---|
| 53 |
self.f = f |
|---|
| 54 |
self.n = n |
|---|
| 55 |
self.linecount = 0 |
|---|
| 56 |
self.headers = 1 |
|---|
| 57 |
self.done = 0 |
|---|
| 58 |
self.buf = '' |
|---|
| 59 |
|
|---|
| 60 |
def read(self, bytes): |
|---|
| 61 |
if self.done: |
|---|
| 62 |
return '' |
|---|
| 63 |
data = self.f.read(bytes) |
|---|
| 64 |
if not data: |
|---|
| 65 |
return data |
|---|
| 66 |
if self.headers: |
|---|
| 67 |
df, sz = data.find('\r\n\r\n'), 4 |
|---|
| 68 |
if df == -1: |
|---|
| 69 |
df, sz = data.find('\n\n'), 2 |
|---|
| 70 |
if df != -1: |
|---|
| 71 |
df += sz |
|---|
| 72 |
val = data[:df] |
|---|
| 73 |
data = data[df:] |
|---|
| 74 |
self.linecount = 1 |
|---|
| 75 |
self.headers = 0 |
|---|
| 76 |
else: |
|---|
| 77 |
val = '' |
|---|
| 78 |
if self.linecount > 0: |
|---|
| 79 |
dsplit = (self.buf+data).split('\n') |
|---|
| 80 |
self.buf = dsplit[-1] |
|---|
| 81 |
for ln in dsplit[:-1]: |
|---|
| 82 |
if self.linecount > self.n: |
|---|
| 83 |
self.done = 1 |
|---|
| 84 |
return val |
|---|
| 85 |
val += (ln + '\n') |
|---|
| 86 |
self.linecount += 1 |
|---|
| 87 |
return val |
|---|
| 88 |
else: |
|---|
| 89 |
return data |
|---|
| 90 |
|
|---|
| 91 |
|
|---|
| 92 |
|
|---|
| 93 |
class _POP3MessageDeleted(Exception): |
|---|
| 94 |
""" |
|---|
| 95 |
Internal control-flow exception. Indicates the file of a deleted message |
|---|
| 96 |
was requested. |
|---|
| 97 |
""" |
|---|
| 98 |
|
|---|
| 99 |
|
|---|
| 100 |
class POP3Error(Exception): |
|---|
| 101 |
pass |
|---|
| 102 |
|
|---|
| 103 |
|
|---|
| 104 |
|
|---|
| 105 |
class _IteratorBuffer(object): |
|---|
| 106 |
bufSize = 0 |
|---|
| 107 |
|
|---|
| 108 |
def __init__(self, write, iterable, memoryBufferSize=None): |
|---|
| 109 |
""" |
|---|
| 110 |
Create a _IteratorBuffer. |
|---|
| 111 |
|
|---|
| 112 |
@param write: A one-argument callable which will be invoked with a list |
|---|
| 113 |
of strings which have been buffered. |
|---|
| 114 |
|
|---|
| 115 |
@param iterable: The source of input strings as any iterable. |
|---|
| 116 |
|
|---|
| 117 |
@param memoryBufferSize: The upper limit on buffered string length, |
|---|
| 118 |
beyond which the buffer will be flushed to the writer. |
|---|
| 119 |
""" |
|---|
| 120 |
self.lines = [] |
|---|
| 121 |
self.write = write |
|---|
| 122 |
self.iterator = iter(iterable) |
|---|
| 123 |
if memoryBufferSize is None: |
|---|
| 124 |
memoryBufferSize = 2 ** 16 |
|---|
| 125 |
self.memoryBufferSize = memoryBufferSize |
|---|
| 126 |
|
|---|
| 127 |
|
|---|
| 128 |
def __iter__(self): |
|---|
| 129 |
return self |
|---|
| 130 |
|
|---|
| 131 |
|
|---|
| 132 |
def next(self): |
|---|
| 133 |
try: |
|---|
| 134 |
v = self.iterator.next() |
|---|
| 135 |
except StopIteration: |
|---|
| 136 |
if self.lines: |
|---|
| 137 |
self.write(self.lines) |
|---|
| 138 |
|
|---|
| 139 |
del self.iterator, self.lines, self.write |
|---|
| 140 |
raise |
|---|
| 141 |
else: |
|---|
| 142 |
if v is not None: |
|---|
| 143 |
self.lines.append(v) |
|---|
| 144 |
self.bufSize += len(v) |
|---|
| 145 |
if self.bufSize > self.memoryBufferSize: |
|---|
| 146 |
self.write(self.lines) |
|---|
| 147 |
self.lines = [] |
|---|
| 148 |
self.bufSize = 0 |
|---|
| 149 |
|
|---|
| 150 |
|
|---|
| 151 |
|
|---|
| 152 |
def iterateLineGenerator(proto, gen): |
|---|
| 153 |
""" |
|---|
| 154 |
Hook the given protocol instance up to the given iterator with an |
|---|
| 155 |
_IteratorBuffer and schedule the result to be exhausted via the protocol. |
|---|
| 156 |
|
|---|
| 157 |
@type proto: L{POP3} |
|---|
| 158 |
@type gen: iterator |
|---|
| 159 |
@rtype: L{twisted.internet.defer.Deferred} |
|---|
| 160 |
""" |
|---|
| 161 |
coll = _IteratorBuffer(proto.transport.writeSequence, gen) |
|---|
| 162 |
return proto.schedule(coll) |
|---|
| 163 |
|
|---|
| 164 |
|
|---|
| 165 |
|
|---|
| 166 |
def successResponse(response): |
|---|
| 167 |
""" |
|---|
| 168 |
Format the given object as a positive response. |
|---|
| 169 |
""" |
|---|
| 170 |
response = str(response) |
|---|
| 171 |
return '+OK %s\r\n' % (response,) |
|---|
| 172 |
|
|---|
| 173 |
|
|---|
| 174 |
|
|---|
| 175 |
def formatStatResponse(msgs): |
|---|
| 176 |
""" |
|---|
| 177 |
Format the list of message sizes appropriately for a STAT response. |
|---|
| 178 |
|
|---|
| 179 |
Yields None until it finishes computing a result, then yields a str |
|---|
| 180 |
instance that is suitable for use as a response to the STAT command. |
|---|
| 181 |
Intended to be used with a L{twisted.internet.task.Cooperator}. |
|---|
| 182 |
""" |
|---|
| 183 |
i = 0 |
|---|
| 184 |
bytes = 0 |
|---|
| 185 |
for size in msgs: |
|---|
| 186 |
i += 1 |
|---|
| 187 |
bytes += size |
|---|
| 188 |
yield None |
|---|
| 189 |
yield successResponse('%d %d' % (i, bytes)) |
|---|
| 190 |
|
|---|
| 191 |
|
|---|
| 192 |
|
|---|
| 193 |
def formatListLines(msgs): |
|---|
| 194 |
""" |
|---|
| 195 |
Format a list of message sizes appropriately for the lines of a LIST |
|---|
| 196 |
response. |
|---|
| 197 |
|
|---|
| 198 |
Yields str instances formatted appropriately for use as lines in the |
|---|
| 199 |
response to the LIST command. Does not include the trailing '.'. |
|---|
| 200 |
""" |
|---|
| 201 |
i = 0 |
|---|
| 202 |
for size in msgs: |
|---|
| 203 |
i += 1 |
|---|
| 204 |
yield '%d %d\r\n' % (i, size) |
|---|
| 205 |
|
|---|
| 206 |
|
|---|
| 207 |
|
|---|
| 208 |
def formatListResponse(msgs): |
|---|
| 209 |
""" |
|---|
| 210 |
Format a list of message sizes appropriately for a complete LIST response. |
|---|
| 211 |
|
|---|
| 212 |
Yields str instances formatted appropriately for use as a LIST command |
|---|
| 213 |
response. |
|---|
| 214 |
""" |
|---|
| 215 |
yield successResponse(len(msgs)) |
|---|
| 216 |
for ele in formatListLines(msgs): |
|---|
| 217 |
yield ele |
|---|
| 218 |
yield '.\r\n' |
|---|
| 219 |
|
|---|
| 220 |
|
|---|
| 221 |
|
|---|
| 222 |
def formatUIDListLines(msgs, getUidl): |
|---|
| 223 |
""" |
|---|
| 224 |
Format the list of message sizes appropriately for the lines of a UIDL |
|---|
| 225 |
response. |
|---|
| 226 |
|
|---|
| 227 |
Yields str instances formatted appropriately for use as lines in the |
|---|
| 228 |
response to the UIDL command. Does not include the trailing '.'. |
|---|
| 229 |
""" |
|---|
| 230 |
for i, m in enumerate(msgs): |
|---|
| 231 |
if m is not None: |
|---|
| 232 |
uid = getUidl(i) |
|---|
| 233 |
yield '%d %s\r\n' % (i + 1, uid) |
|---|
| 234 |
|
|---|
| 235 |
|
|---|
| 236 |
|
|---|
| 237 |
def formatUIDListResponse(msgs, getUidl): |
|---|
| 238 |
""" |
|---|
| 239 |
Format a list of message sizes appropriately for a complete UIDL response. |
|---|
| 240 |
|
|---|
| 241 |
Yields str instances formatted appropriately for use as a UIDL command |
|---|
| 242 |
response. |
|---|
| 243 |
""" |
|---|
| 244 |
yield successResponse('') |
|---|
| 245 |
for ele in formatUIDListLines(msgs, getUidl): |
|---|
| 246 |
yield ele |
|---|
| 247 |
yield '.\r\n' |
|---|
| 248 |
|
|---|
| 249 |
|
|---|
| 250 |
|
|---|
| 251 |
class POP3(basic.LineOnlyReceiver, policies.TimeoutMixin): |
|---|
| 252 |
""" |
|---|
| 253 |
POP3 server protocol implementation. |
|---|
| 254 |
|
|---|
| 255 |
@ivar portal: A reference to the L{twisted.cred.portal.Portal} instance we |
|---|
| 256 |
will authenticate through. |
|---|
| 257 |
|
|---|
| 258 |
@ivar factory: A L{twisted.mail.pop3.IServerFactory} which will be used to |
|---|
| 259 |
determine some extended behavior of the server. |
|---|
| 260 |
|
|---|
| 261 |
@ivar timeOut: An integer which defines the minimum amount of time which |
|---|
| 262 |
may elapse without receiving any traffic after which the client will be |
|---|
| 263 |
disconnected. |
|---|
| 264 |
|
|---|
| 265 |
@ivar schedule: A one-argument callable which should behave like |
|---|
| 266 |
L{twisted.internet.task.coiterate}. |
|---|
| 267 |
""" |
|---|
| 268 |
implements(interfaces.IProducer) |
|---|
| 269 |
|
|---|
| 270 |
magic = None |
|---|
| 271 |
_userIs = None |
|---|
| 272 |
_onLogout = None |
|---|
| 273 |
|
|---|
| 274 |
AUTH_CMDS = ['CAPA', 'USER', 'PASS', 'APOP', 'AUTH', 'RPOP', 'QUIT'] |
|---|
| 275 |
|
|---|
| 276 |
portal = None |
|---|
| 277 |
factory = None |
|---|
| 278 |
|
|---|
| 279 |
|
|---|
| 280 |
mbox = None |
|---|
| 281 |
|
|---|
| 282 |
|
|---|
| 283 |
|
|---|
| 284 |
timeOut = 300 |
|---|
| 285 |
|
|---|
| 286 |
|
|---|
| 287 |
state = "COMMAND" |
|---|
| 288 |
|
|---|
| 289 |
|
|---|
| 290 |
blocked = None |
|---|
| 291 |
|
|---|
| 292 |
|
|---|
| 293 |
schedule = staticmethod(task.coiterate) |
|---|
| 294 |
|
|---|
| 295 |
|
|---|
| 296 |
_highest = 0 |
|---|
| 297 |
|
|---|
| 298 |
def connectionMade(self): |
|---|
| 299 |
if self.magic is None: |
|---|
| 300 |
self.magic = self.generateMagic() |
|---|
| 301 |
self.successResponse(self.magic) |
|---|
| 302 |
self.setTimeout(self.timeOut) |
|---|
| 303 |
if getattr(self.factory, 'noisy', True): |
|---|
| 304 |
log.msg("New connection from " + str(self.transport.getPeer())) |
|---|
| 305 |
|
|---|
| 306 |
|
|---|
| 307 |
def connectionLost(self, reason): |
|---|
| 308 |
if self._onLogout is not None: |
|---|
| 309 |
self._onLogout() |
|---|
| 310 |
self._onLogout = None |
|---|
| 311 |
self.setTimeout(None) |
|---|
| 312 |
|
|---|
| 313 |
|
|---|
| 314 |
def generateMagic(self): |
|---|
| 315 |
return smtp.messageid() |
|---|
| 316 |
|
|---|
| 317 |
|
|---|
| 318 |
def successResponse(self, message=''): |
|---|
| 319 |
self.transport.write(successResponse(message)) |
|---|
| 320 |
|
|---|
| 321 |
def failResponse(self, message=''): |
|---|
| 322 |
self.sendLine('-ERR ' + str(message)) |
|---|
| 323 |
|
|---|
| 324 |
|
|---|
| 325 |
|
|---|
| 326 |
|
|---|
| 327 |
|
|---|
| 328 |
def lineReceived(self, line): |
|---|
| 329 |
|
|---|
| 330 |
self.resetTimeout() |
|---|
| 331 |
getattr(self, 'state_' + self.state)(line) |
|---|
| 332 |
|
|---|
| 333 |
def _unblock(self, _): |
|---|
| 334 |
commands = self.blocked |
|---|
| 335 |
self.blocked = None |
|---|
| 336 |
while commands and self.blocked is None: |
|---|
| 337 |
cmd, args = commands.pop(0) |
|---|
| 338 |
self.processCommand(cmd, *args) |
|---|
| 339 |
if self.blocked is not None: |
|---|
| 340 |
self.blocked.extend(commands) |
|---|
| 341 |
|
|---|
| 342 |
def state_COMMAND(self, line): |
|---|
| 343 |
try: |
|---|
| 344 |
return self.processCommand(*line.split(' ')) |
|---|
| 345 |
except (ValueError, AttributeError, POP3Error, TypeError), e: |
|---|
| 346 |
log.err() |
|---|
| 347 |
self.failResponse('bad protocol or server: %s: %s' % (e.__class__.__name__, e)) |
|---|
| 348 |
|
|---|
| 349 |
def processCommand(self, command, *args): |
|---|
| 350 |
if self.blocked is not None: |
|---|
| 351 |
self.blocked.append((command, args)) |
|---|
| 352 |
return |
|---|
| 353 |
|
|---|
| 354 |
command = string.upper(command) |
|---|
| 355 |
authCmd = command in self.AUTH_CMDS |
|---|
| 356 |
if not self.mbox and not authCmd: |
|---|
| 357 |
raise POP3Error("not authenticated yet: cannot do " + command) |
|---|
| 358 |
f = getattr(self, 'do_' + command, None) |
|---|
| 359 |
if f: |
|---|
| 360 |
return f(*args) |
|---|
| 361 |
raise POP3Error("Unknown protocol command: " + command) |
|---|
| 362 |
|
|---|
| 363 |
|
|---|
| 364 |
def listCapabilities(self): |
|---|
| 365 |
baseCaps = [ |
|---|
| 366 |
"TOP", |
|---|
| 367 |
"USER", |
|---|
| 368 |
"UIDL", |
|---|
| 369 |
"PIPELINE", |
|---|
| 370 |
"CELERITY", |
|---|
| 371 |
"AUSPEX", |
|---|
| 372 |
"POTENCE", |
|---|
| 373 |
] |
|---|
| 374 |
|
|---|
| 375 |
if IServerFactory.providedBy(self.factory): |
|---|
| 376 |
|
|---|
| 377 |
|
|---|
| 378 |
try: |
|---|
| 379 |
v = self.factory.cap_IMPLEMENTATION() |
|---|
| 380 |
except NotImplementedError: |
|---|
| 381 |
pass |
|---|
| 382 |
except: |
|---|
| 383 |
log.err() |
|---|
| 384 |
else: |
|---|
| 385 |
baseCaps.append("IMPLEMENTATION " + str(v)) |
|---|
| 386 |
|
|---|
| 387 |
try: |
|---|
| 388 |
v = self.factory.cap_EXPIRE() |
|---|
| 389 |
except NotImplementedError: |
|---|
| 390 |
pass |
|---|
| 391 |
except: |
|---|
| 392 |
log.err() |
|---|
| 393 |
else: |
|---|
| 394 |
if v is None: |
|---|
| 395 |
v = "NEVER" |
|---|
| 396 |
if self.factory.perUserExpiration(): |
|---|
| 397 |
if self.mbox: |
|---|
| 398 |
v = str(self.mbox.messageExpiration) |
|---|
| 399 |
else: |
|---|
| 400 |
v = str(v) + " USER" |
|---|
| 401 |
v = str(v) |
|---|
| 402 |
baseCaps.append("EXPIRE " + v) |
|---|
| 403 |
|
|---|
| 404 |
try: |
|---|
| 405 |
v = self.factory.cap_LOGIN_DELAY() |
|---|
| 406 |
except NotImplementedError: |
|---|
| 407 |
pass |
|---|
| 408 |
except: |
|---|
| 409 |
log.err() |
|---|
| 410 |
else: |
|---|
| 411 |
if self.factory.perUserLoginDelay(): |
|---|
| 412 |
if self.mbox: |
|---|
| 413 |
v = str(self.mbox.loginDelay) |
|---|
| 414 |
else: |
|---|
| 415 |
v = str(v) + " USER" |
|---|
| 416 |
v = str(v) |
|---|
| 417 |
baseCaps.append("LOGIN-DELAY " + v) |
|---|
| 418 |
|
|---|
| 419 |
try: |
|---|
| 420 |
v = self.factory.challengers |
|---|
| 421 |
except AttributeError: |
|---|
| 422 |
pass |
|---|
| 423 |
except: |
|---|
| 424 |
log.err() |
|---|
| 425 |
else: |
|---|
| 426 |
baseCaps.append("SASL " + ' '.join(v.keys())) |
|---|
| 427 |
return baseCaps |
|---|
| 428 |
|
|---|
| 429 |
def do_CAPA(self): |
|---|
| 430 |
self.successResponse("I can do the following:") |
|---|
| 431 |
for cap in self.listCapabilities(): |
|---|
| 432 |
self.sendLine(cap) |
|---|
| 433 |
self.sendLine(".") |
|---|
| 434 |
|
|---|
| 435 |
def do_AUTH(self, args=None): |
|---|
| 436 |
if not getattr(self.factory, 'challengers', None): |
|---|
| 437 |
self.failResponse("AUTH extension unsupported") |
|---|
| 438 |
return |
|---|
| 439 |
|
|---|
| 440 |
if args is None: |
|---|
| 441 |
self.successResponse("Supported authentication methods:") |
|---|
| 442 |
for a in self.factory.challengers: |
|---|
| 443 |
self.sendLine(a.upper()) |
|---|
| 444 |
self.sendLine(".") |
|---|
| 445 |
return |
|---|
| 446 |
|
|---|
| 447 |
auth = self.factory.challengers.get(args.strip().upper()) |
|---|
| 448 |
if not self.portal or not auth: |
|---|
| 449 |
self.failResponse("Unsupported SASL selected") |
|---|
| 450 |
return |
|---|
| 451 |
|
|---|
| 452 |
self._auth = auth() |
|---|
| 453 |
chal = self._auth.getChallenge() |
|---|
| 454 |
|
|---|
| 455 |
self.sendLine('+ ' + base64.encodestring(chal).rstrip('\n')) |
|---|
| 456 |
self.state = 'AUTH' |
|---|
| 457 |
|
|---|
| 458 |
def state_AUTH(self, line): |
|---|
| 459 |
self.state = "COMMAND" |
|---|
| 460 |
try: |
|---|
| 461 |
parts = base64.decodestring(line).split(None, 1) |
|---|
| 462 |
except binascii.Error: |
|---|
| 463 |
self.failResponse("Invalid BASE64 encoding") |
|---|
| 464 |
else: |
|---|
| 465 |
if len(parts) != 2: |
|---|
| 466 |
self.failResponse("Invalid AUTH response") |
|---|
| 467 |
return |
|---|
| 468 |
self._auth.username = parts[0] |
|---|
| 469 |
self._auth.response = parts[1] |
|---|
| 470 |
d = self.portal.login(self._auth, None, IMailbox) |
|---|
| 471 |
d.addCallback(self._cbMailbox, parts[0]) |
|---|
| 472 |
d.addErrback(self._ebMailbox) |
|---|
| 473 |
d.addErrback(self._ebUnexpected) |
|---|
| 474 |
|
|---|
| 475 |
def do_APOP(self, user, digest): |
|---|
| 476 |
d = defer.maybeDeferred(self.authenticateUserAPOP, user, digest) |
|---|
| 477 |
d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,) |
|---|
| 478 |
).addErrback(self._ebUnexpected) |
|---|
| 479 |
|
|---|
| 480 |
def _cbMailbox(self, (interface, avatar, logout), user): |
|---|
| 481 |
if interface is not IMailbox: |
|---|
| 482 |
self.failResponse('Authentication failed') |
|---|
| 483 |
log.err("_cbMailbox() called with an interface other than IMailbox") |
|---|
| 484 |
return |
|---|
| 485 |
|
|---|
| 486 |
self.mbox = avatar |
|---|
| 487 |
self._onLogout = logout |
|---|
| 488 |
self.successResponse('Authentication succeeded') |
|---|
| 489 |
if getattr(self.factory, 'noisy', True): |
|---|
| 490 |
log.msg("Authenticated login for " + user) |
|---|
| 491 |
|
|---|
| 492 |
def _ebMailbox(self, failure): |
|---|
| 493 |
failure = failure.trap(cred.error.LoginDenied, cred.error.LoginFailed) |
|---|
| 494 |
if issubclass(failure, cred.error.LoginDenied): |
|---|
| 495 |
self.failResponse("Access denied: " + str(failure)) |
|---|
| 496 |
elif issubclass(failure, cred.error.LoginFailed): |
|---|
| 497 |
self.failResponse('Authentication failed') |
|---|
| 498 |
if getattr(self.factory, 'noisy', True): |
|---|
| 499 |
log.msg("Denied login attempt from " + str(self.transport.getPeer())) |
|---|
| 500 |
|
|---|
| 501 |
def _ebUnexpected(self, failure): |
|---|
| 502 |
self.failResponse('Server error: ' + failure.getErrorMessage()) |
|---|
| 503 |
log.err(failure) |
|---|
| 504 |
|
|---|
| 505 |
def do_USER(self, user): |
|---|
| 506 |
self._userIs = user |
|---|
| 507 |
self.successResponse('USER accepted, send PASS') |
|---|
| 508 |
|
|---|
| 509 |
def do_PASS(self, password): |
|---|
| 510 |
if self._userIs is None: |
|---|
| 511 |
self.failResponse("USER required before PASS") |
|---|
| 512 |
return |
|---|
| 513 |
user = self._userIs |
|---|
| 514 |
self._userIs = None |
|---|
| 515 |
d = defer.maybeDeferred(self.authenticateUserPASS, user, password) |
|---|
| 516 |
d.addCallbacks(self._cbMailbox, self._ebMailbox, callbackArgs=(user,) |
|---|
| 517 |
).addErrback(self._ebUnexpected) |
|---|
| 518 |
|
|---|
| 519 |
|
|---|
| 520 |
def _longOperation(self, d): |
|---|
| 521 |
|
|---|
| 522 |
|
|---|
| 523 |
timeOut = self.timeOut |
|---|
| 524 |
self.setTimeout(None) |
|---|
| 525 |
self.blocked = [] |
|---|
| 526 |
d.addCallback(self._unblock) |
|---|
| 527 |
d.addCallback(lambda ign: self.setTimeout(timeOut)) |
|---|
| 528 |
return d |
|---|
| 529 |
|
|---|
| 530 |
|
|---|
| 531 |
def _coiterate(self, gen): |
|---|
| 532 |
return self.schedule(_IteratorBuffer(self.transport.writeSequence, gen)) |
|---|
| 533 |
|
|---|
| 534 |
|
|---|
| 535 |
def do_STAT(self): |
|---|
| 536 |
d = defer.maybeDeferred(self.mbox.listMessages) |
|---|
| 537 |
def cbMessages(msgs): |
|---|
| 538 |
return self._coiterate(formatStatResponse(msgs)) |
|---|
| 539 |
def ebMessages(err): |
|---|
| 540 |
self.failResponse(err.getErrorMessage()) |
|---|
| 541 |
log.msg("Unexpected do_STAT failure:") |
|---|
| 542 |
log.err(err) |
|---|
| 543 |
return self._longOperation(d.addCallbacks(cbMessages, ebMessages)) |
|---|
| 544 |
|
|---|
| 545 |
|
|---|
| 546 |
def do_LIST(self, i=None): |
|---|
| 547 |
if i is None: |
|---|
| 548 |
d = defer.maybeDeferred(self.mbox.listMessages) |
|---|
| 549 |
def cbMessages(msgs): |
|---|
| 550 |
return self._coiterate(formatListResponse(msgs)) |
|---|
| 551 |
def ebMessages(err): |
|---|
| 552 |
self.failResponse(err.getErrorMessage()) |
|---|
| 553 |
log.msg("Unexpected do_LIST failure:") |
|---|
| 554 |
log.err(err) |
|---|
| 555 |
return self._longOperation(d.addCallbacks(cbMessages, ebMessages)) |
|---|
| 556 |
else: |
|---|
| 557 |
try: |
|---|
| 558 |
i = int(i) |
|---|
| 559 |
if i < 1: |
|---|
| 560 |
raise ValueError() |
|---|
| 561 |
except ValueError: |
|---|
| 562 |
self.failResponse("Invalid message-number: %r" % (i,)) |
|---|
| 563 |
else: |
|---|
| 564 |
d = defer.maybeDeferred(self.mbox.listMessages, i - 1) |
|---|
| 565 |
def cbMessage(msg): |
|---|
| 566 |
self.successResponse('%d %d' % (i, msg)) |
|---|
| 567 |
def ebMessage(err): |
|---|
| 568 |
errcls = err.check(ValueError, IndexError) |
|---|
| 569 |
if errcls is not None: |
|---|
| 570 |
if errcls is IndexError: |
|---|
| 571 |
|
|---|
| 572 |
|
|---|
| 573 |
|
|---|
| 574 |
warnings.warn( |
|---|
| 575 |
"twisted.mail.pop3.IMailbox.listMessages may not " |
|---|
| 576 |
"raise IndexError for out-of-bounds message numbers: " |
|---|
| 577 |
"raise ValueError instead.", |
|---|
| 578 |
PendingDeprecationWarning) |
|---|
| 579 |
self.failResponse("Invalid message-number: %r" % (i,)) |
|---|
| 580 |
else: |
|---|
| 581 |
self.failResponse(err.getErrorMessage()) |
|---|
| 582 |
log.msg("Unexpected do_LIST failure:") |
|---|
| 583 |
log.err(err) |
|---|
| 584 |
return self._longOperation(d.addCallbacks(cbMessage, ebMessage)) |
|---|
| 585 |
|
|---|
| 586 |
|
|---|
| 587 |
def do_UIDL(self, i=None): |
|---|
| 588 |
if i is None: |
|---|
| 589 |
d = defer.maybeDeferred(self.mbox.listMessages) |
|---|
| 590 |
def cbMessages(msgs): |
|---|
| 591 |
return self._coiterate(formatUIDListResponse(msgs, self.mbox.getUidl)) |
|---|
| 592 |
def ebMessages(err): |
|---|
| 593 |
self.failResponse(err.getErrorMessage()) |
|---|
| 594 |
log.msg("Unexpected do_UIDL failure:") |
|---|
| 595 |
log.err(err) |
|---|
| 596 |
return self._longOperation(d.addCallbacks(cbMessages, ebMessages)) |
|---|
| 597 |
else: |
|---|
| 598 |
try: |
|---|
| 599 |
i = int(i) |
|---|
| 600 |
if i < 1: |
|---|
| 601 |
raise ValueError() |
|---|
| 602 |
except ValueError: |
|---|
| 603 |
self.failResponse("Bad message number argument") |
|---|
| 604 |
else: |
|---|
| 605 |
try: |
|---|
| 606 |
msg = self.mbox.getUidl(i - 1) |
|---|
| 607 |
except IndexError: |
|---|
| 608 |
|
|---|
| 609 |
warnings.warn( |
|---|
| 610 |
"twisted.mail.pop3.IMailbox.getUidl may not " |
|---|
| 611 |
"raise IndexError for out-of-bounds message numbers: " |
|---|
| 612 |
"raise ValueError instead.", |
|---|
| 613 |
PendingDeprecationWarning) |
|---|
| 614 |
self.failResponse("Bad message number argument") |
|---|
| 615 |
except ValueError: |
|---|
| 616 |
self.failResponse("Bad message number argument") |
|---|
| 617 |
else: |
|---|
| 618 |
self.successResponse(str(msg)) |
|---|
| 619 |
|
|---|
| 620 |
|
|---|
| 621 |
def _getMessageFile(self, i): |
|---|
| 622 |
""" |
|---|
| 623 |
Retrieve the size and contents of a given message, as a two-tuple. |
|---|
| 624 |
|
|---|
| 625 |
@param i: The number of the message to operate on. This is a base-ten |
|---|
| 626 |
string representation starting at 1. |
|---|
| 627 |
|
|---|
| 628 |
@return: A Deferred which fires with a two-tuple of an integer and a |
|---|
| 629 |
file-like object. |
|---|
| 630 |
""" |
|---|
| 631 |
try: |
|---|
| 632 |
msg = int(i) - 1 |
|---|
| 633 |
if msg < 0: |
|---|
| 634 |
raise ValueError() |
|---|
| 635 |
except ValueError: |
|---|
| 636 |
self.failResponse("Bad message number argument") |
|---|
| 637 |
return defer.succeed(None) |
|---|
| 638 |
|
|---|
| 639 |
sizeDeferred = defer.maybeDeferred(self.mbox.listMessages, msg) |
|---|
| 640 |
def cbMessageSize(size): |
|---|
| 641 |
if not size: |
|---|
| 642 |
return defer.fail(_POP3MessageDeleted()) |
|---|
| 643 |
fileDeferred = defer.maybeDeferred(self.mbox.getMessage, msg) |
|---|
| 644 |
fileDeferred.addCallback(lambda fObj: (size, fObj)) |
|---|
| 645 |
return fileDeferred |
|---|
| 646 |
|
|---|
| 647 |
def ebMessageSomething(err): |
|---|
| 648 |
errcls = err.check(_POP3MessageDeleted, ValueError, IndexError) |
|---|
| 649 |
if errcls is _POP3MessageDeleted: |
|---|
| 650 |
self.failResponse("message deleted") |
|---|
| 651 |
elif errcls in (ValueError, IndexError): |
|---|
| 652 |
if errcls is IndexError: |
|---|
| 653 |
|
|---|
| 654 |
warnings.warn( |
|---|
| 655 |
"twisted.mail.pop3.IMailbox.listMessages may not " |
|---|
| 656 |
"raise IndexError for out-of-bounds message numbers: " |
|---|
| 657 |
"raise ValueError instead.", |
|---|
| 658 |
PendingDeprecationWarning) |
|---|
| 659 |
self.failResponse("Bad message number argument") |
|---|
| 660 |
else: |
|---|
| 661 |
log.msg("Unexpected _getMessageFile failure:") |
|---|
| 662 |
log.err(err) |
|---|
| 663 |
return None |
|---|
| 664 |
|
|---|
| 665 |
sizeDeferred.addCallback(cbMessageSize) |
|---|
| 666 |
sizeDeferred.addErrback(ebMessageSomething) |
|---|
| 667 |
return sizeDeferred |
|---|
| 668 |
|
|---|
| 669 |
|
|---|
| 670 |
def _sendMessageContent(self, i, fpWrapper, successResponse): |
|---|
| 671 |
d = self._getMessageFile(i) |
|---|
| 672 |
def cbMessageFile(info): |
|---|
| 673 |
if info is None: |
|---|
| 674 |
|
|---|
| 675 |
|
|---|
| 676 |
return |
|---|
| 677 |
|
|---|
| 678 |
self._highest = max(self._highest, int(i)) |
|---|
| 679 |
resp, fp = info |
|---|
| 680 |
fp = fpWrapper(fp) |
|---|
| 681 |
self.successResponse(successResponse(resp)) |
|---|
| 682 |
s = basic.FileSender() |
|---|
| 683 |
d = s.beginFileTransfer(fp, self.transport, self.transformChunk) |
|---|
| 684 |
|
|---|
| 685 |
def cbFileTransfer(lastsent): |
|---|
| 686 |
if lastsent != '\n': |
|---|
| 687 |
line = '\r\n.' |
|---|
| 688 |
else: |
|---|
| 689 |
line = '.' |
|---|
| 690 |
self.sendLine(line) |
|---|
| 691 |
|
|---|
| 692 |
def ebFileTransfer(err): |
|---|
| 693 |
self.transport.loseConnection() |
|---|
| 694 |
log.msg("Unexpected error in _sendMessageContent:") |
|---|
| 695 |
log.err(err) |
|---|
| 696 |
|
|---|
| 697 |
d.addCallback(cbFileTransfer) |
|---|
| 698 |
d.addErrback(ebFileTransfer) |
|---|
| 699 |
return d |
|---|
| 700 |
return self._longOperation(d.addCallback(cbMessageFile)) |
|---|
| 701 |
|
|---|
| 702 |
|
|---|
| 703 |
def do_TOP(self, i, size): |
|---|
| 704 |
try: |
|---|
| 705 |
size = int(size) |
|---|
| 706 |
if size < 0: |
|---|
| 707 |
raise ValueError |
|---|
| 708 |
except ValueError: |
|---|
| 709 |
self.failResponse("Bad line count argument") |
|---|
| 710 |
else: |
|---|
| 711 |
return self._sendMessageContent( |
|---|
| 712 |
i, |
|---|
| 713 |
lambda fp: _HeadersPlusNLines(fp, size), |
|---|
| 714 |
lambda size: "Top of message follows") |
|---|
| 715 |
|
|---|
| 716 |
|
|---|
| 717 |
def do_RETR(self, i): |
|---|
| 718 |
return self._sendMessageContent( |
|---|
| 719 |
i, |
|---|
| 720 |
lambda fp: fp, |
|---|
| 721 |
lambda size: "%d" % (size,)) |
|---|
| 722 |
|
|---|
| 723 |
|
|---|
| 724 |
def transformChunk(self, chunk): |
|---|
| 725 |
return chunk.replace('\n', '\r\n').replace('\r\n.', '\r\n..') |
|---|
| 726 |
|
|---|
| 727 |
|
|---|
| 728 |
def finishedFileTransfer(self, lastsent): |
|---|
| 729 |
if lastsent != '\n': |
|---|
| 730 |
line = '\r\n.' |
|---|
| 731 |
else: |
|---|
| 732 |
line = '.' |
|---|
| 733 |
self.sendLine(line) |
|---|
| 734 |
|
|---|
| 735 |
|
|---|
| 736 |
def do_DELE(self, i): |
|---|
| 737 |
i = int(i)-1 |
|---|
| 738 |
self.mbox.deleteMessage(i) |
|---|
| 739 |
self.successResponse() |
|---|
| 740 |
|
|---|
| 741 |
|
|---|
| 742 |
def do_NOOP(self): |
|---|
| 743 |
"""Perform no operation. Return a success code""" |
|---|
| 744 |
self.successResponse() |
|---|
| 745 |
|
|---|
| 746 |
|
|---|
| 747 |
def do_RSET(self): |
|---|
| 748 |
"""Unset all deleted message flags""" |
|---|
| 749 |
try: |
|---|
| 750 |
self.mbox.undeleteMessages() |
|---|
| 751 |
except: |
|---|
| 752 |
log.err() |
|---|
| 753 |
self.failResponse() |
|---|
| 754 |
else: |
|---|
| 755 |
self._highest = 0 |
|---|
| 756 |
self.successResponse() |
|---|
| 757 |
|
|---|
| 758 |
|
|---|
| 759 |
def do_LAST(self): |
|---|
| 760 |
""" |
|---|
| 761 |
Return the index of the highest message yet downloaded. |
|---|
| 762 |
""" |
|---|
| 763 |
self.successResponse(self._highest) |
|---|
| 764 |
|
|---|
| 765 |
|
|---|
| 766 |
def do_RPOP(self, user): |
|---|
| 767 |
self.failResponse('permission denied, sucker') |
|---|
| 768 |
|
|---|
| 769 |
|
|---|
| 770 |
def do_QUIT(self): |
|---|
| 771 |
if self.mbox: |
|---|
| 772 |
self.mbox.sync() |
|---|
| 773 |
self.successResponse() |
|---|
| 774 |
self.transport.loseConnection() |
|---|
| 775 |
|
|---|
| 776 |
|
|---|
| 777 |
def authenticateUserAPOP(self, user, digest): |
|---|
| 778 |
"""Perform authentication of an APOP login. |
|---|
| 779 |
|
|---|
| 780 |
@type user: C{str} |
|---|
| 781 |
@param user: The name of the user attempting to log in. |
|---|
| 782 |
|
|---|
| 783 |
@type digest: C{str} |
|---|
| 784 |
@param digest: The response string with which the user replied. |
|---|
| 785 |
|
|---|
| 786 |
@rtype: C{Deferred} |
|---|
| 787 |
@return: A deferred whose callback is invoked if the login is |
|---|
| 788 |
successful, and whose errback will be invoked otherwise. The |
|---|
| 789 |
callback will be passed a 3-tuple consisting of IMailbox, |
|---|
| 790 |
an object implementing IMailbox, and a zero-argument callable |
|---|
| 791 |
to be invoked when this session is terminated. |
|---|
| 792 |
""" |
|---|
| 793 |
if self.portal is not None: |
|---|
| 794 |
return self.portal.login( |
|---|
| 795 |
APOPCredentials(self.magic, user, digest), |
|---|
| 796 |
None, |
|---|
| 797 |
IMailbox |
|---|
| 798 |
) |
|---|
| 799 |
raise cred.error.UnauthorizedLogin() |
|---|
| 800 |
|
|---|
| 801 |
def authenticateUserPASS(self, user, password): |
|---|
| 802 |
"""Perform authentication of a username/password login. |
|---|
| 803 |
|
|---|
| 804 |
@type user: C{str} |
|---|
| 805 |
@param user: The name of the user attempting to log in. |
|---|
| 806 |
|
|---|
| 807 |
@type password: C{str} |
|---|
| 808 |
@param password: The password to attempt to authenticate with. |
|---|
| 809 |
|
|---|
| 810 |
@rtype: C{Deferred} |
|---|
| 811 |
@return: A deferred whose callback is invoked if the login is |
|---|
| 812 |
successful, and whose errback will be invoked otherwise. The |
|---|
| 813 |
callback will be passed a 3-tuple consisting of IMailbox, |
|---|
| 814 |
an object implementing IMailbox, and a zero-argument callable |
|---|
| 815 |
to be invoked when this session is terminated. |
|---|
| 816 |
""" |
|---|
| 817 |
if self.portal is not None: |
|---|
| 818 |
return self.portal.login( |
|---|
| 819 |
cred.credentials.UsernamePassword(user, password), |
|---|
| 820 |
None, |
|---|
| 821 |
IMailbox |
|---|
| 822 |
) |
|---|
| 823 |
raise cred.error.UnauthorizedLogin() |
|---|
| 824 |
|
|---|
| 825 |
|
|---|
| 826 |
class IServerFactory(Interface): |
|---|
| 827 |
"""Interface for querying additional parameters of this POP3 server. |
|---|
| 828 |
|
|---|
| 829 |
Any cap_* method may raise NotImplementedError if the particular |
|---|
| 830 |
capability is not supported. If cap_EXPIRE() does not raise |
|---|
| 831 |
NotImplementedError, perUserExpiration() must be implemented, otherwise |
|---|
| 832 |
they are optional. If cap_LOGIN_DELAY() is implemented, |
|---|
| 833 |
perUserLoginDelay() must be implemented, otherwise they are optional. |
|---|
| 834 |
|
|---|
| 835 |
@ivar challengers: A dictionary mapping challenger names to classes |
|---|
| 836 |
implementing C{IUsernameHashedPassword}. |
|---|
| 837 |
""" |
|---|
| 838 |
|
|---|
| 839 |
def cap_IMPLEMENTATION(): |
|---|
| 840 |
"""Return a string describing this POP3 server implementation.""" |
|---|
| 841 |
|
|---|
| 842 |
def cap_EXPIRE(): |
|---|
| 843 |
"""Return the minimum number of days messages are retained.""" |
|---|
| 844 |
|
|---|
| 845 |
def perUserExpiration(): |
|---|
| 846 |
"""Indicate whether message expiration is per-user. |
|---|
| 847 |
|
|---|
| 848 |
@return: True if it is, false otherwise. |
|---|
| 849 |
""" |
|---|
| 850 |
|
|---|
| 851 |
def cap_LOGIN_DELAY(): |
|---|
| 852 |
"""Return the minimum number of seconds between client logins.""" |
|---|
| 853 |
|
|---|
| 854 |
def perUserLoginDelay(): |
|---|
| 855 |
"""Indicate whether the login delay period is per-user. |
|---|
| 856 |
|
|---|
| 857 |
@return: True if it is, false otherwise. |
|---|
| 858 |
""" |
|---|
| 859 |
|
|---|
| 860 |
class IMailbox(Interface): |
|---|
| 861 |
""" |
|---|
| 862 |
@type loginDelay: C{int} |
|---|
| 863 |
@ivar loginDelay: The number of seconds between allowed logins for the |
|---|
| 864 |
user associated with this mailbox. None |
|---|
| 865 |
|
|---|
| 866 |
@type messageExpiration: C{int} |
|---|
| 867 |
@ivar messageExpiration: The number of days messages in this mailbox will |
|---|
| 868 |
remain on the server before being deleted. |
|---|
| 869 |
""" |
|---|
| 870 |
|
|---|
| 871 |
def listMessages(index=None): |
|---|
| 872 |
"""Retrieve the size of one or more messages. |
|---|
| 873 |
|
|---|
| 874 |
@type index: C{int} or C{None} |
|---|
| 875 |
@param index: The number of the message for which to retrieve the |
|---|
| 876 |
size (starting at 0), or None to retrieve the size of all messages. |
|---|
| 877 |
|
|---|
| 878 |
@rtype: C{int} or any iterable of C{int} or a L{Deferred} which fires |
|---|
| 879 |
with one of these. |
|---|
| 880 |
|
|---|
| 881 |
@return: The number of octets in the specified message, or an iterable |
|---|
| 882 |
of integers representing the number of octets in all the messages. Any |
|---|
| 883 |
value which would have referred to a deleted message should be set to 0. |
|---|
| 884 |
|
|---|
| 885 |
@raise ValueError: if C{index} is greater than the index of any message |
|---|
| 886 |
in the mailbox. |
|---|
| 887 |
""" |
|---|
| 888 |
|
|---|
| 889 |
def getMessage(index): |
|---|
| 890 |
"""Retrieve a file-like object for a particular message. |
|---|
| 891 |
|
|---|
| 892 |
@type index: C{int} |
|---|
| 893 |
@param index: The number of the message to retrieve |
|---|
| 894 |
|
|---|
| 895 |
@rtype: A file-like object |
|---|
| 896 |
@return: A file containing the message data with lines delimited by |
|---|
| 897 |
C{\\n}. |
|---|
| 898 |
""" |
|---|
| 899 |
|
|---|
| 900 |
def getUidl(index): |
|---|
| 901 |
"""Get a unique identifier for a particular message. |
|---|
| 902 |
|
|---|
| 903 |
@type index: C{int} |
|---|
| 904 |
@param index: The number of the message for which to retrieve a UIDL |
|---|
| 905 |
|
|---|
| 906 |
@rtype: C{str} |
|---|
| 907 |
@return: A string of printable characters uniquely identifying for all |
|---|
| 908 |
time the specified message. |
|---|
| 909 |
|
|---|
| 910 |
@raise ValueError: if C{index} is greater than the index of any message |
|---|
| 911 |
in the mailbox. |
|---|
| 912 |
""" |
|---|
| 913 |
|
|---|
| 914 |
def deleteMessage(index): |
|---|
| 915 |
"""Delete a particular message. |
|---|
| 916 |
|
|---|
| 917 |
This must not change the number of messages in this mailbox. Further |
|---|
| 918 |
requests for the size of deleted messages should return 0. Further |
|---|
| 919 |
requests for the message itself may raise an exception. |
|---|
| 920 |
|
|---|
| 921 |
@type index: C{int} |
|---|
| 922 |
@param index: The number of the message to delete. |
|---|
| 923 |
""" |
|---|
| 924 |
|
|---|
| 925 |
def undeleteMessages(): |
|---|
| 926 |
""" |
|---|
| 927 |
Undelete any messages which have been marked for deletion since the |
|---|
| 928 |
most recent L{sync} call. |
|---|
| 929 |
|
|---|
| 930 |
Any message which can be undeleted should be returned to its |
|---|
| 931 |
original position in the message sequence and retain its original |
|---|
| 932 |
UID. |
|---|
| 933 |
""" |
|---|
| 934 |
|
|---|
| 935 |
def sync(): |
|---|
| 936 |
"""Perform checkpointing. |
|---|
| 937 |
|
|---|
| 938 |
This method will be called to indicate the mailbox should attempt to |
|---|
| 939 |
clean up any remaining deleted messages. |
|---|
| 940 |
""" |
|---|
| 941 |
|
|---|
| 942 |
|
|---|
| 943 |
|
|---|
| 944 |
class Mailbox: |
|---|
| 945 |
implements(IMailbox) |
|---|
| 946 |
|
|---|
| 947 |
def listMessages(self, i=None): |
|---|
| 948 |
return [] |
|---|
| 949 |
def getMessage(self, i): |
|---|
| 950 |
raise ValueError |
|---|
| 951 |
def getUidl(self, i): |
|---|
| 952 |
raise ValueError |
|---|
| 953 |
def deleteMessage(self, i): |
|---|
| 954 |
raise ValueError |
|---|
| 955 |
def undeleteMessages(self): |
|---|
| 956 |
pass |
|---|
| 957 |
def sync(self): |
|---|
| 958 |
pass |
|---|
| 959 |
|
|---|
| 960 |
|
|---|
| 961 |
NONE, SHORT, FIRST_LONG, LONG = range(4) |
|---|
| 962 |
|
|---|
| 963 |
NEXT = {} |
|---|
| 964 |
NEXT[NONE] = NONE |
|---|
| 965 |
NEXT[SHORT] = NONE |
|---|
| 966 |
NEXT[FIRST_LONG] = LONG |
|---|
| 967 |
NEXT[LONG] = NONE |
|---|
| 968 |
|
|---|
| 969 |
class POP3Client(basic.LineOnlyReceiver): |
|---|
| 970 |
|
|---|
| 971 |
mode = SHORT |
|---|
| 972 |
command = 'WELCOME' |
|---|
| 973 |
import re |
|---|
| 974 |
welcomeRe = re.compile('<(.*)>') |
|---|
| 975 |
|
|---|
| 976 |
def __init__(self): |
|---|
| 977 |
import warnings |
|---|
| 978 |
warnings.warn("twisted.mail.pop3.POP3Client is deprecated, " |
|---|
| 979 |
"please use twisted.mail.pop3.AdvancedPOP3Client " |
|---|
| 980 |
"instead.", DeprecationWarning, |
|---|
| 981 |
stacklevel=3) |
|---|
| 982 |
|
|---|
| 983 |
def sendShort(self, command, params=None): |
|---|
| 984 |
if params is not None: |
|---|
| 985 |
self.sendLine('%s %s' % (command, params)) |
|---|
| 986 |
else: |
|---|
| 987 |
self.sendLine(command) |
|---|
| 988 |
self.command = command |
|---|
| 989 |
self.mode = SHORT |
|---|
| 990 |
|
|---|
| 991 |
def sendLong(self, command, params): |
|---|
| 992 |
if params: |
|---|
| 993 |
self.sendLine('%s %s' % (command, params)) |
|---|
| 994 |
else: |
|---|
| 995 |
self.sendLine(command) |
|---|
| 996 |
self.command = command |
|---|
| 997 |
self.mode = FIRST_LONG |
|---|
| 998 |
|
|---|
| 999 |
def handle_default(self, line): |
|---|
| 1000 |
if line[:-4] == '-ERR': |
|---|
| 1001 |
self.mode = NONE |
|---|
| 1002 |
|
|---|
| 1003 |
def handle_WELCOME(self, line): |
|---|
| 1004 |
code, data = line.split(' ', 1) |
|---|
| 1005 |
if code != '+OK': |
|---|
| 1006 |
self.transport.loseConnection() |
|---|
| 1007 |
else: |
|---|
| 1008 |
m = self.welcomeRe.match(line) |
|---|
| 1009 |
if m: |
|---|
| 1010 |
self.welcomeCode = m.group(1) |
|---|
| 1011 |
|
|---|
| 1012 |
def _dispatch(self, command, default, *args): |
|---|
| 1013 |
try: |
|---|
| 1014 |
method = getattr(self, 'handle_'+command, default) |
|---|
| 1015 |
if method is not None: |
|---|
| 1016 |
method(*args) |
|---|
| 1017 |
except: |
|---|
| 1018 |
log.err() |
|---|
| 1019 |
|
|---|
| 1020 |
def lineReceived(self, line): |
|---|
| 1021 |
if self.mode == SHORT or self.mode == FIRST_LONG: |
|---|
| 1022 |
self.mode = NEXT[self.mode] |
|---|
| 1023 |
self._dispatch(self.command, self.handle_default, line) |
|---|
| 1024 |
elif self.mode == LONG: |
|---|
| 1025 |
if line == '.': |
|---|
| 1026 |
self.mode = NEXT[self.mode] |
|---|
| 1027 |
self._dispatch(self.command+'_end', None) |
|---|
| 1028 |
return |
|---|
| 1029 |
if line[:1] == '.': |
|---|
| 1030 |
line = line[1:] |
|---|
| 1031 |
self._dispatch(self.command+"_continue", None, line) |
|---|
| 1032 |
|
|---|
| 1033 |
def apopAuthenticate(self, user, password, magic): |
|---|
| 1034 |
digest = md5(magic + password).hexdigest() |
|---|
| 1035 |
self.apop(user, digest) |
|---|
| 1036 |
|
|---|
| 1037 |
def apop(self, user, digest): |
|---|
| 1038 |
self.sendLong('APOP', ' '.join((user, digest))) |
|---|
| 1039 |
def retr(self, i): |
|---|
| 1040 |
self.sendLong('RETR', i) |
|---|
| 1041 |
def dele(self, i): |
|---|
| 1042 |
self.sendShort('DELE', i) |
|---|
| 1043 |
def list(self, i=''): |
|---|
| 1044 |
self.sendLong('LIST', i) |
|---|
| 1045 |
def uidl(self, i=''): |
|---|
| 1046 |
self.sendLong('UIDL', i) |
|---|
| 1047 |
def user(self, name): |
|---|
| 1048 |
self.sendShort('USER', name) |
|---|
| 1049 |
def pass_(self, pass_): |
|---|
| 1050 |
self.sendShort('PASS', pass_) |
|---|
| 1051 |
def quit(self): |
|---|
| 1052 |
self.sendShort('QUIT') |
|---|
| 1053 |
|
|---|
| 1054 |
from twisted.mail.pop3client import POP3Client as AdvancedPOP3Client |
|---|
| 1055 |
from twisted.mail.pop3client import POP3ClientError |
|---|
| 1056 |
from twisted.mail.pop3client import InsecureAuthenticationDisallowed |
|---|
| 1057 |
from twisted.mail.pop3client import ServerErrorResponse |
|---|
| 1058 |
from twisted.mail.pop3client import LineTooLong |
|---|
| 1059 |
|
|---|
| 1060 |
__all__ = [ |
|---|
| 1061 |
|
|---|
| 1062 |
'IMailbox', 'IServerFactory', |
|---|
| 1063 |
|
|---|
| 1064 |
|
|---|
| 1065 |
'POP3Error', 'POP3ClientError', 'InsecureAuthenticationDisallowed', |
|---|
| 1066 |
'ServerErrorResponse', 'LineTooLong', |
|---|
| 1067 |
|
|---|
| 1068 |
|
|---|
| 1069 |
'POP3', 'POP3Client', 'AdvancedPOP3Client', |
|---|
| 1070 |
|
|---|
| 1071 |
|
|---|
| 1072 |
'APOPCredentials', 'Mailbox'] |
|---|