|
2 | 2 | import struct |
3 | 3 | from abc import abstractmethod, ABC |
4 | 4 | from enum import Enum |
| 5 | +from smtplib import SMTP, SMTPException |
5 | 6 | from typing import ClassVar, Optional |
6 | 7 |
|
7 | 8 |
|
@@ -65,19 +66,34 @@ def __init__(self, smtp_ehlo_hostname: str): |
65 | 66 | self._smtp_ehlo_hostname = smtp_ehlo_hostname |
66 | 67 |
|
67 | 68 | def prepare_socket_for_tls_handshake(self, sock: socket.socket) -> None: |
68 | | - # Get the SMTP banner |
69 | | - sock.recv(2048) |
| 69 | + # SMTP parsing has some complicated areas and some unusual but legal |
| 70 | + # server behavior - this code uses Python's smtplib to handle the protocol. |
| 71 | + smtp = SMTP(local_hostname=self._smtp_ehlo_hostname) |
| 72 | + smtp.sock = sock |
70 | 73 |
|
71 | | - # Send a EHLO and wait for the 250 status |
72 | | - sock.send(f"EHLO {self._smtp_ehlo_hostname}\r\n".encode("ascii")) |
73 | | - data = sock.recv(2048) |
74 | | - if b"250 " not in data: |
75 | | - raise OpportunisticTlsError(f"SMTP EHLO was rejected: {repr(data)}") |
| 74 | + try: |
| 75 | + code, message = smtp.getreply() |
| 76 | + except SMTPException as exc: |
| 77 | + code, message = -1, str(exc) |
| 78 | + if code != 220: |
| 79 | + raise OpportunisticTlsError(f"Unable to find 220 service ready response: {repr(message)}") |
| 80 | + |
| 81 | + try: |
| 82 | + code, message = smtp.ehlo() |
| 83 | + except SMTPException as exc: |
| 84 | + code, message = -1, str(exc) |
| 85 | + if code != 250: |
| 86 | + raise OpportunisticTlsError(f"SMTP EHLO was rejected: {repr(message)}") |
| 87 | + |
| 88 | + if not smtp.has_extn("starttls"): |
| 89 | + raise OpportunisticTlsError(f"Server does not support STARTTLS: {repr(message)}") |
76 | 90 |
|
77 | | - # Send a STARTTLS |
78 | | - sock.send(b"STARTTLS\r\n") |
79 | | - if b"220" not in sock.recv(2048): |
80 | | - raise OpportunisticTlsError("SMTP STARTTLS not supported") |
| 91 | + try: |
| 92 | + code, message = smtp.docmd("STARTTLS") |
| 93 | + except SMTPException as exc: |
| 94 | + code, message = -1, str(exc) |
| 95 | + if code != 220: |
| 96 | + raise OpportunisticTlsError(f"SMTP STARTTLS rejected: {message}") |
81 | 97 |
|
82 | 98 |
|
83 | 99 | class _XmppHelper(_OpportunisticTlsHelper): |
@@ -223,16 +239,14 @@ class _PostgresHelper(_GenericOpportunisticTlsHelper): |
223 | 239 |
|
224 | 240 |
|
225 | 241 | def get_opportunistic_tls_helper( |
226 | | - protocol: ProtocolWithOpportunisticTlsEnum, xmpp_to_hostname: Optional[str], smtp_ehlo_hostname: str |
| 242 | + protocol: ProtocolWithOpportunisticTlsEnum, xmpp_to_hostname: Optional[str] |
227 | 243 | ) -> _OpportunisticTlsHelper: |
228 | 244 | helper_cls = _START_TLS_HELPER_CLASSES[protocol] |
229 | | - if protocol in [ProtocolWithOpportunisticTlsEnum.XMPP, ProtocolWithOpportunisticTlsEnum.XMPP_SERVER]: |
| 245 | + if protocol not in [ProtocolWithOpportunisticTlsEnum.XMPP, ProtocolWithOpportunisticTlsEnum.XMPP_SERVER]: |
| 246 | + opportunistic_tls_helper = helper_cls() |
| 247 | + else: |
230 | 248 | if xmpp_to_hostname is None: |
231 | 249 | raise ValueError("Received None for xmpp_to_hostname") |
232 | 250 | opportunistic_tls_helper = helper_cls(xmpp_to=xmpp_to_hostname) |
233 | | - elif protocol == ProtocolWithOpportunisticTlsEnum.SMTP: |
234 | | - opportunistic_tls_helper = helper_cls(smtp_ehlo_hostname) |
235 | | - else: |
236 | | - opportunistic_tls_helper = helper_cls() |
237 | 251 |
|
238 | 252 | return opportunistic_tls_helper |
0 commit comments