Поддержка прокси-сервера Python smtplib

Я хотел бы отправить письмо через прокси.

Моя текущая реализация заключается в следующем.

Я подключаюсь к серверу smtp с аутентификацией. После успешного входа я отправляю письмо. Это работает нормально, но когда я смотрю на заголовок письма, я вижу свое имя хоста. Я хотел бы вместо этого туннелировать через прокси.

Любая помощь будет высоко оценена.

9 ответов

Используйте SocksiPy:

import smtplib
import socks

#'proxy_port' should be an integer
#'PROXY_TYPE_SOCKS4' can be replaced to HTTP or PROXY_TYPE_SOCKS5
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS4, proxy_host, proxy_port)
socks.wrapmodule(smtplib)

smtp = smtplib.SMTP()
...

У меня вчера была похожая проблема, это код, который я написал для решения проблемы. Это незаметно позволяет вам использовать все методы smtp через прокси.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#       smtprox.py
#       Shouts to suidrewt
#
# ############################################# #
# This module allows Proxy support in MailFux.  #
# Shouts to Betrayed for telling me about       #
# http CONNECT                                  #
# ############################################# #

import smtplib
import socket

def recvline(sock):
    stop = 0
    line = ''
    while True:
        i = sock.recv(1)
        if i == '\n': stop = 1
        line += i
        if stop == 1:
            break
    return line

class ProxSMTP( smtplib.SMTP ):

    def __init__(self, host='', port=0, p_address='',p_port=0, local_hostname=None,
             timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
        """Initialize a new instance.

        If specified, `host' is the name of the remote host to which to
        connect.  If specified, `port' specifies the port to which to connect.
        By default, smtplib.SMTP_PORT is used.  An SMTPConnectError is raised
        if the specified `host' doesn't respond correctly.  If specified,
        `local_hostname` is used as the FQDN of the local host.  By default,
        the local hostname is found using socket.getfqdn().

        """
        self.p_address = p_address
        self.p_port = p_port

        self.timeout = timeout
        self.esmtp_features = {}
        self.default_port = smtplib.SMTP_PORT
        if host:
            (code, msg) = self.connect(host, port)
            if code != 220:
                raise SMTPConnectError(code, msg)
        if local_hostname is not None:
            self.local_hostname = local_hostname
        else:
            # RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and
            # if that can't be calculated, that we should use a domain literal
            # instead (essentially an encoded IP address like [A.B.C.D]).
            fqdn = socket.getfqdn()
            if '.' in fqdn:
                self.local_hostname = fqdn
            else:
                # We can't find an fqdn hostname, so use a domain literal
                addr = '127.0.0.1'
                try:
                    addr = socket.gethostbyname(socket.gethostname())
                except socket.gaierror:
                    pass
                self.local_hostname = '[%s]' % addr
        smtplib.SMTP.__init__(self)

    def _get_socket(self, port, host, timeout):
        # This makes it simpler for SMTP_SSL to use the SMTP connect code
        # and just alter the socket connection bit.
        if self.debuglevel > 0: print>>stderr, 'connect:', (host, port)
        new_socket = socket.create_connection((self.p_address,self.p_port), timeout)
        new_socket.sendall("CONNECT {0}:{1} HTTP/1.1\r\n\r\n".format(port,host))
        for x in xrange(2): recvline(new_socket)
        return new_socket

Как отметили mkerrig и Denis Cornehl в комментарии к другому ответу, PySocks create_connection с модифицированным классом SMTP из smtplib работает без необходимости подключать сокеты monkeypatch для всего.

Я все еще ненавижу эту реализацию (кто знает, что сломается с другой версией python или smtplib), но пока это работает (3.8.1). Поскольку мне не удалось найти какие-либо другие решения в Интернете, которые работали, вот моя попытка:

  1. Скопируйте функции init и _get_socket из класса smtplib.SMTP
  2. Измените init, чтобы добавить proxy_addr и proxy_port
  3. Измените _get_socket так, чтобы он возвращал socks.create_connection() (vs socket)
  4. Измените SMTPConnectError на smtplib.SMTPConnectError, чтобы он работал

my_proxy_smtplib.py:

      import socket
import smtplib

import socks


class ProxySMTP(smtplib.SMTP):
    def __init__(self, host='', port=0, local_hostname=None,
                 timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
                 source_address=None, proxy_addr=None, proxy_port=None):
        """Initialize a new instance.

        If specified, `host' is the name of the remote host to which to
        connect.  If specified, `port' specifies the port to which to connect.
        By default, smtplib.SMTP_PORT is used.  If a host is specified the
        connect method is called, and if it returns anything other than a
        success code an SMTPConnectError is raised.  If specified,
        `local_hostname` is used as the FQDN of the local host in the HELO/EHLO
        command.  Otherwise, the local hostname is found using
        socket.getfqdn(). The `source_address` parameter takes a 2-tuple (host,
        port) for the socket to bind to as its source address before
        connecting. If the host is '' and port is 0, the OS default behavior
        will be used.

        """
        self._host = host
        self.timeout = timeout
        self.esmtp_features = {}
        self.command_encoding = 'ascii'
        self.source_address = source_address
        self.proxy_addr = proxy_addr
        self.proxy_port = proxy_port

        if host:
            (code, msg) = self.connect(host, port)
            if code != 220:
                self.close()
                raise smtplib.SMTPConnectError(code, msg)
        if local_hostname is not None:
            self.local_hostname = local_hostname
        else:
            # RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and
            # if that can't be calculated, that we should use a domain literal
            # instead (essentially an encoded IP address like [A.B.C.D]).
            fqdn = socket.getfqdn()
            if '.' in fqdn:
                self.local_hostname = fqdn
            else:
                # We can't find an fqdn hostname, so use a domain literal
                addr = '127.0.0.1'
                try:
                    addr = socket.gethostbyname(socket.gethostname())
                except socket.gaierror:
                    pass
                self.local_hostname = '[%s]' % addr

    def _get_socket(self, host, port, timeout):
        # This makes it simpler for SMTP_SSL to use the SMTP connect code
        # and just alter the socket connection bit.
        if self.debuglevel > 0:
            self._print_debug('connect: to', (host, port), self.source_address)
        return socks.create_connection((host, port),
                                       proxy_type=socks.PROXY_TYPE_SOCKS5,
                                       timeout=timeout,
                                       proxy_addr=self.proxy_addr,
                                       proxy_port=self.proxy_port)

И использовать:

      from my_proxy_smtplib import ProxySMTP

email_server = ProxySMTP('smtp.gmail.com', 587,
                         proxy_addr='192.168.0.1',
                         proxy_port=3487)
email_server.starttls()
email_server.login(user_email, user_pass)
email_server.sendmail(user_email, recipient_list, msg.as_string())
email_server.quit()

Гораздо более простой подход, который исправляет smtplibТолько:

      proxy_url = urlparse('http://user:pass@10.0.0.1:8080')

def _smtplib_get_socket(self, host, port, timeout):
    # Patched SMTP._get_socket
    return socks.create_connection(
        (host, port),
        timeout,
        self.source_address,
        proxy_type=socks.HTTP,
        proxy_addr=proxy_url.hostname,
        proxy_port=int(proxy_url.port),
        proxy_username=proxy_url.username,
        proxy_password=proxy_url.password,
    )

# We do this instead of wrapmodule due to
# https://github.com/Anorov/PySocks/issues/158
smtplib.SMTP._get_socket = _smtplib_get_socket

Для тех, кому все еще нужно :) Я сделал рабочее решение с Python3 и PySocks:

      # -*- coding: utf-8 -*-
import smtplib, socks, re, os, logging
from urllib.request import getproxies
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.application import MIMEApplication

# ============================================================= #

# global proxy config dictionary
PROXY = {"useproxy": True, "server": None, "port": None, "type": "HTTP", "username": None, "password": None}
# ============================================================= #

class Proxifier:
    """
    Helper class to configure proxy settings. Exposes the `get_socket()` method that returns 
    a proxified connection (socket).
    """

    def __init__(self, proxy_server=None, proxy_port=None, proxy_type='HTTP', proxy_username=None, proxy_password=None):
        # proxy type: HTTP, SOCKS4 or SOCKS5 (default = HTTP)
        self.proxy_type = {'HTTP': socks.HTTP, 'SOCKS4': socks.SOCKS4, 'SOCKS5': socks.SOCKS5}.get(proxy_type, socks.HTTP)
        # proxy auth if required
        self.proxy_username = proxy_username
        self.proxy_password = proxy_password
        # if host or port not set, attempt to retrieve from system
        if not proxy_server or not proxy_port:
            self._get_sysproxy()
        else:
            self.proxy_server = proxy_server
            self.proxy_port = proxy_port        

    def _get_sysproxy(self, setvars=True):
        """
        Retrieves system proxy settings from OS environment variables (HTTP_PROXY, HTTPS_PROXY etc.)
        If `setvars` == `True`, sets the member variables as well.
        """
        proxy_server, proxy_port, proxy_username, proxy_password = (None, None, None, None)
        template = re.compile(r'^(((?P<user>[^:]+)(:(?P<pass>[^@]*)?))@)?(?P<host>[^:]+?)(:(?P<port>\d{1,5})?)$', re.I)
        try:
            sys_proxy = getproxies()
            for p in sys_proxy:
                if p.lower().startswith('http') or p.lower().startswith('socks'):
                    sp = sys_proxy[p].split('//')
                    sp = sp[1] if len(sp) > 1 else sp[0]
                    m = template.fullmatch(sp)
                    proxy_server = m.group('host') or None
                    try:
                        proxy_port = int(m.group('port')) or None
                    except:
                        pass
                    proxy_username = m.group('user') or None
                    proxy_password = m.group('pass') or None
                    break
        except Exception as err:
            logging.exception(err)

        if setvars:
            self.proxy_server = proxy_server or self.proxy_server
            self.proxy_port = proxy_port or self.proxy_port
            self.proxy_username = proxy_username or self.proxy_username
            self.proxy_password = proxy_password or self.proxy_password
        return (proxy_server, proxy_port)

    def get_socket(self, source_address, host, port, timeout=None):
        """
        Applies proxy settings to PySocks `create_connection()` method to
        created a proxified connection (socket) which can be used by other
        interfaces to establish connection.
        """
        return socks.create_connection((host, port), timeout, source_address, 
                                       proxy_type=self.proxy_type, proxy_addr=self.proxy_server, proxy_port=self.proxy_port, 
                                       proxy_username=self.proxy_username, proxy_password=self.proxy_password)

    @staticmethod
    def get_proxifier(proxy=PROXY):
        """
        Factory returns a `Proxifier` object given proxy settings in a dictionary.
        """
        if not proxy or not proxy.get('useproxy', False):
            return None
        return Proxifier(proxy.get('server', None), proxy.get('port', None), proxy.get('type', None), 
                        proxy.get('username', None), proxy.get('password', None))

# ============================================================= #                                       

class SMTP_Proxy(smtplib.SMTP):
    """
    Descendant of SMTP with optional proxy wrapping.
    """

    def __init__(self, host='', port=0, local_hostname=None, timeout=object(), source_address=None, 
                 proxifier: Proxifier=None):
        # `Proxifier` object if proxy is required
        self._proxifier = proxifier
        super().__init__(host, port, local_hostname, timeout, source_address)        

    def _get_socket(self, host, port, timeout):
        """
        Overridden method of base class to allow for proxified connection.
        """
        if not self._proxifier:
            # no proxy: use base class implementation
            return super()._get_socket(host, port, timeout)
        if timeout is not None and not timeout:
            raise ValueError('Non-blocking socket (timeout=0) is not supported')
        if self.debuglevel > 0:
            self._print_debug('connect: to', (host, port), self.source_address)   
        # proxy: use proxifier connection     
        return self._proxifier.get_socket(self.source_address, host, port, timeout)

# ============================================================= #

class SMTP_SSL_Proxy(smtplib.SMTP_SSL):
    """
    Descendant of SMTP_SSL with optional proxy wrapping.
    """

    def __init__(self, host='', port=0, local_hostname=None, keyfile=None, certfile=None, timeout=object(), source_address=None, context=None, 
                 proxifier: Proxifier=None):     
        # `Proxifier` object if proxy is required   
        self._proxifier = proxifier
        super().__init__(host, port, local_hostname, keyfile, certfile, timeout, source_address, context)

    def _get_socket(self, host, port, timeout):
        """
        Overridden method of base class to allow for proxified connection.
        """
        if not self._proxifier:
            # no proxy: use base class implementation
            return super()._get_socket(host, port, timeout)
        if timeout is not None and not timeout:
            raise ValueError('Non-blocking socket (timeout=0) is not supported')
        if self.debuglevel > 0:
            self._print_debug('connect: to', (host, port), self.source_address)
        # proxy: use proxifier connection
        newsocket = self._proxifier.get_socket(self.source_address, host, port, timeout)
        return self.context.wrap_socket(newsocket, server_hostname=self._host)

# ============================================================= #

def send_email(body, subject, sender, receivers, smtp, proxy=PROXY, sender_name='Appname', attachments=None):
    """
    Sends email with optional attachments and proxy settings.
    """
    is_ssl = smtp['protocol'].upper() == 'SSL'
    smtp_class = SMTP_SSL_Proxy if is_ssl else SMTP_Proxy
    try:
        msg = MIMEMultipart()
        msg['Subject'] = subject
        msg['To'] = ', '.join(receivers)
        # msg['Bcc'] = ', '.join(receivers)
        msg['From'] = f'{sender_name} <{sender}>' if sender_name else sender
        msg.attach(MIMEText(body))

        if attachments:
            for filepath in attachments:
                bname = os.path.basename(filepath)
                try:
                    with open(filepath, 'rb') as file_:
                        part = MIMEApplication(file_.read(), Name=bname)
                    part['Content-Disposition'] = f'attachment; filename="{bname}"'
                    msg.attach(part)
                except:
                    continue

        with smtp_class(smtp['server'], smtp['port'], proxifier=Proxifier.get_proxifier(proxy)) as emailer:
            emailer.login(smtp['login'], smtp['password'])
            if not is_ssl: 
                emailer.starttls()
            emailer.sendmail(sender, receivers, msg.as_string())

        logging.debug(f"--- Email sent to: {receivers}")

    except smtplib.SMTPException as smtp_err:
        logging.exception(smtp_err)

    except Exception as err:
        logging.exception(err)

Этот код заработал у меня. 1. Имя файла не должно быть email.py. Переименуйте имя файла, например, emailSend.py. 2. Необходимо разрешить Google отправлять сообщения из ненадежных источников.

У меня была эта проблема некоторое время назад, вот как я ее решил. В этом примере Tor используется в качестве прокси-сервера Socks5.

      import socks
import socket
from ssl import create_default_context
from smtplib import SMTP_SSL

socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, '127.0.0.1', 9050)
socket.socket = socks.socksocket

context = create_default_context()
ssl_socket = context.wrap_socket(socket.socket(), server_hostname=<your_smtp_server>)

smtp_server = SMTP_SSL(<your_smtp_server>, 465)
smtp_server.login(<your_smtp_user>, <your_smtp_password>)
  • Socks.setdefaultproxy() используется для установки типа прокси по умолчанию SOCKS5 с IP-адресом и портом вашего прокси-сервера socks5.
  • ocket.socket =socks.socksocket присваивает класс socks.socksocket атрибуту socket.socket. Это эффективно заменяет стандартный класс сокетов классом socks.socksocket, позволяя маршрутизировать все последующие операции с сокетами через прокси-сервер socks.
  • context.wrap_socket(socket.socket(), server_hostname=<your_smtp_server>) оборачивает сокет в уровень SSL/TLS, используя предоставленный контекст SSL. При этом создается соединение SSL/TLS с указанным SMTP-сервером.

Я перепробовал множество способов, но обнаружил, что прокси-сервер Nginx SMTP лучше, нет необходимости делать патч для обезьян, вам просто нужно установить Nginx на интернет-компьютер, подключенный к вашей частной сети, конфигурация Nginx такая же.

      stream {
   server {
      listen 25;
      proxy_pass some_specified_smtpserver:25;
   }
}

ссылка: https://docs.nginx.com/nginx/admin-guide/mail-proxy/mail-proxy/

smtplib Модуль не включает в себя функциональность для подключения к SMTP-серверу через HTTP-прокси. Пользовательский класс, опубликованный ryoh, не работал для меня, очевидно, потому что мой HTTP-прокси получает только закодированные сообщения. Я написал следующий пользовательский класс на основе кода ryos, и он работает нормально.

import smtplib
import socket

def recvline(sock):
    """Receives a line."""
    stop = 0
    line = ''
    while True:
        i = sock.recv(1)
        if i.decode('UTF-8') == '\n': stop = 1
        line += i.decode('UTF-8')
        if stop == 1:
            print('Stop reached.')
            break
    print('Received line: %s' % line)
    return line

class ProxySMTP(smtplib.SMTP):
    """Connects to a SMTP server through a HTTP proxy."""

    def __init__(self, host='', port=0, p_address='',p_port=0, local_hostname=None,
             timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
        """Initialize a new instance.

        If specified, `host' is the name of the remote host to which to
        connect.  If specified, `port' specifies the port to which to connect.
        By default, smtplib.SMTP_PORT is used.  An SMTPConnectError is raised
        if the specified `host' doesn't respond correctly.  If specified,
        `local_hostname` is used as the FQDN of the local host.  By default,
        the local hostname is found using socket.getfqdn().

        """
        self.p_address = p_address
        self.p_port = p_port

        self.timeout = timeout
        self.esmtp_features = {}
        self.default_port = smtplib.SMTP_PORT

        if host:
            (code, msg) = self.connect(host, port)
            if code != 220:
                raise IOError(code, msg)

        if local_hostname is not None:
            self.local_hostname = local_hostname
        else:
            # RFC 2821 says we should use the fqdn in the EHLO/HELO verb, and
            # if that can't be calculated, that we should use a domain literal
            # instead (essentially an encoded IP address like [A.B.C.D]).
            fqdn = socket.getfqdn()

            if '.' in fqdn:
                self.local_hostname = fqdn
            else:
                # We can't find an fqdn hostname, so use a domain literal
                addr = '127.0.0.1'

                try:
                    addr = socket.gethostbyname(socket.gethostname())
                except socket.gaierror:
                    pass
                self.local_hostname = '[%s]' % addr

        smtplib.SMTP.__init__(self)

    def _get_socket(self, port, host, timeout):
        # This makes it simpler for SMTP to use the SMTP connect code
        # and just alter the socket connection bit.
        print('Will connect to:', (host, port))
        print('Connect to proxy.')
        new_socket = socket.create_connection((self.p_address,self.p_port), timeout)

        s = "CONNECT %s:%s HTTP/1.1\r\n\r\n" % (port,host)
        s = s.encode('UTF-8')
        new_socket.sendall(s)

        print('Sent CONNECT. Receiving lines.')
        for x in range(2): recvline(new_socket)

        print('Connected.')
        return new_socket

Чтобы подключиться к SMTP-серверу, просто используйте класс ProxySMTP вместо smtplib.SMTP,

proxy_host = YOUR_PROXY_HOST
proxy_port = YOUR_PROXY_PORT

# Both port 25 and 587 work for SMTP
conn = ProxySMTP(host='smtp.gmail.com', port=587,
                 p_address=proxy_host, p_port=proxy_port)

conn.ehlo()
conn.starttls()
conn.ehlo()

r, d = conn.login(YOUR_EMAIL_ADDRESS, YOUR_PASSWORD)

print('Login reply: %s' % r)

sender = 'from@fromdomain.com'
receivers = ['to@todomain.com']

message = """From: From Person <from@fromdomain.com>
To: To Person <to@todomain.com>
Subject: SMTP e-mail test

This is a test e-mail message.
"""

print('Send email.')
conn.sendmail(sender, receivers, message)

print('Success.')
conn.close()
Другие вопросы по тегам