|
13 | 13 |
|
14 | 14 | # Imports |
15 | 15 |
|
| 16 | +import binascii |
16 | 17 | import errno |
17 | 18 | import re |
18 | 19 | import socket |
@@ -47,6 +48,8 @@ class error_proto(Exception): pass |
47 | 48 | # 512 characters, including CRLF. We have selected 2048 just to be on |
48 | 49 | # the safe side. |
49 | 50 | _MAXLINE = 2048 |
| 51 | +# maximum number of AUTH challenges we are willing to process (parity with smtplib) |
| 52 | +_MAXCHALLENGE = 5 |
50 | 53 |
|
51 | 54 |
|
52 | 55 | class POP3: |
@@ -218,61 +221,6 @@ def pass_(self, pswd): |
218 | 221 | """ |
219 | 222 | return self._shortcmd('PASS %s' % pswd) |
220 | 223 |
|
221 | | - def auth(self, mechanism, authobject=None, initial_response=None): |
222 | | - """Authenticate to the POP3 server using the AUTH command (RFC 5034). |
223 | | -
|
224 | | - Result is 'response'. |
225 | | - """ |
226 | | - if authobject is not None and initial_response is not None: |
227 | | - raise ValueError('authobject and initial_response are mutually exclusive') |
228 | | - |
229 | | - if initial_response is not None: |
230 | | - if isinstance(initial_response, str): |
231 | | - initial_response = initial_response.encode(self.encoding) |
232 | | - b64 = base64.b64encode(initial_response).decode('ascii') |
233 | | - return self._shortcmd(f'AUTH {mechanism} {b64}'.rstrip()) |
234 | | - |
235 | | - if authobject is None: |
236 | | - return self._shortcmd(f'AUTH {mechanism}') |
237 | | - |
238 | | - self._putcmd(f'AUTH {mechanism}') |
239 | | - while True: |
240 | | - line, _ = self._getline() |
241 | | - if line.startswith(b'+OK'): |
242 | | - return line |
243 | | - if line.startswith(b'-ERR'): |
244 | | - while self._getline() != b'.\r\n': |
245 | | - pass |
246 | | - raise error_proto(line.decode('ascii', 'replace')) |
247 | | - |
248 | | - if not line.startswith(b'+ '): |
249 | | - raise error_proto(f'malformed challenge line: {line!r}') |
250 | | - |
251 | | - challenge_b64 = line[2:] |
252 | | - challenge_b64 = challenge_b64.rstrip(b'\r\n') |
253 | | - |
254 | | - if challenge_b64: |
255 | | - try: |
256 | | - challenge = base64.b64decode(challenge_b64) |
257 | | - except Exception: |
258 | | - padded = challenge_b64 + b'=' * (-len(challenge_b64) % 4) |
259 | | - challenge = base64.b64decode(padded) |
260 | | - else: |
261 | | - challenge = b'' |
262 | | - |
263 | | - response = authobject(challenge) |
264 | | - if response is None: |
265 | | - response = b'' |
266 | | - if isinstance(response, str): |
267 | | - response = response.encode(self.encoding) |
268 | | - |
269 | | - if response == b'*': |
270 | | - self._putcmd('*') |
271 | | - err_line, _ = self._getline() |
272 | | - raise error_proto(err_line.decode('ascii', 'replace')) |
273 | | - |
274 | | - self._putcmd(base64.b64encode(response).decode('ascii')) |
275 | | - |
276 | 224 | def stat(self): |
277 | 225 | """Get mailbox status. |
278 | 226 |
|
@@ -479,6 +427,88 @@ def stls(self, context=None): |
479 | 427 | self._tls_established = True |
480 | 428 | return resp |
481 | 429 |
|
| 430 | + def auth(self, mechanism, authobject, *, initial_response_ok=True): |
| 431 | + """Authenticate to the POP3 server using AUTH (RFC 5034). |
| 432 | +
|
| 433 | + Result is 'response'. |
| 434 | + """ |
| 435 | + mech = mechanism.upper() |
| 436 | + |
| 437 | + initial = None |
| 438 | + if initial_response_ok: |
| 439 | + try: |
| 440 | + initial = authobject() |
| 441 | + except TypeError: |
| 442 | + initial = None |
| 443 | + if isinstance(initial, str): |
| 444 | + initial = initial.encode('ascii', 'strict') |
| 445 | + if initial is not None and not isinstance(initial, (bytes, bytearray)): |
| 446 | + raise TypeError('authobject() must return str or bytes for initial response') |
| 447 | + |
| 448 | + if initial is not None: |
| 449 | + b64 = base64.b64encode(initial).decode('ascii') |
| 450 | + cmd = f'AUTH {mech} {b64}' |
| 451 | + if len(cmd.encode('ascii')) + 2 <= 255: |
| 452 | + self._putcmd(cmd) |
| 453 | + else: |
| 454 | + self._putcmd(f'AUTH {mech}') |
| 455 | + else: |
| 456 | + self._putcmd(f'AUTH {mech}') |
| 457 | + |
| 458 | + auth_challenge_count = 0 |
| 459 | + while True: |
| 460 | + line, _ = self._getline() |
| 461 | + |
| 462 | + if line.startswith(b'+OK'): |
| 463 | + return line |
| 464 | + |
| 465 | + if line.startswith(b'-ERR'): |
| 466 | + raise error_proto(line.decode('ascii', 'replace')) |
| 467 | + # Challenge line: "+ <b64>" or just "+" (empty challenge) |
| 468 | + if not (line == b'+' or line.startswith(b'+ ')): |
| 469 | + raise error_proto(f'malformed AUTH challenge line: {line!r}') |
| 470 | + |
| 471 | + auth_challenge_count += 1 |
| 472 | + if auth_challenge_count > _MAXCHALLENGE: |
| 473 | + raise error_proto('Server AUTH mechanism infinite loop') |
| 474 | + |
| 475 | + chal = line[1:] |
| 476 | + if chal.startswith(b' '): |
| 477 | + chal = chal[1:] |
| 478 | + chal = chal.rstrip(b'\r\n') |
| 479 | + if chal: |
| 480 | + try: |
| 481 | + challenge = base64.b64decode(chal, validate=True) |
| 482 | + except (binascii.Error, ValueError): |
| 483 | + self._putcmd('*') |
| 484 | + line, _ = self._getline() |
| 485 | + raise error_proto(line.decode('ascii', 'replace')) |
| 486 | + else: |
| 487 | + challenge = b'' |
| 488 | + |
| 489 | + resp = authobject(challenge) |
| 490 | + if resp is None: |
| 491 | + resp = b'' |
| 492 | + if isinstance(resp, str): |
| 493 | + resp = resp.encode('ascii', 'strict') |
| 494 | + if not isinstance(resp, (bytes, bytearray)): |
| 495 | + raise TypeError('authobject(challenge) must return str or bytes') |
| 496 | + |
| 497 | + if resp == b'*': |
| 498 | + self._putcmd('*') |
| 499 | + else: |
| 500 | + self._putcmd(base64.b64encode(resp).decode('ascii')) |
| 501 | + |
| 502 | + def auth_plain(self, user, password, authzid=''): |
| 503 | + """Return an authobject suitable for SASL PLAIN. |
| 504 | +
|
| 505 | + Result is 'str'. |
| 506 | + """ |
| 507 | + def _auth_plain(challenge=None): |
| 508 | + # Per RFC 4616, the response is: authzid UTF8 NUL authcid UTF8 NUL passwd UTF8 |
| 509 | + return f"{authzid}\0{user}\0{password}" |
| 510 | + return _auth_plain |
| 511 | + |
482 | 512 |
|
483 | 513 | if HAVE_SSL: |
484 | 514 |
|
@@ -513,6 +543,7 @@ def stls(self, context=None): |
513 | 543 | SSL/TLS session. |
514 | 544 | """ |
515 | 545 | raise error_proto('-ERR TLS session already established') |
| 546 | + |
516 | 547 |
|
517 | 548 | __all__.append("POP3_SSL") |
518 | 549 |
|
|
0 commit comments