Python smtplib 代理支持

Posted

技术标签:

【中文标题】Python smtplib 代理支持【英文标题】:Python smtplib proxy support 【发布时间】:2011-07-11 12:29:33 【问题描述】:

我想通过代理发送电子邮件。

我目前的实现如下:

我通过身份验证连接到 smtp 服务器。成功登录后,我会发送一封电子邮件。它工作正常,但是当我查看电子邮件标题时,我可以看到我的主机名。我想改为通过代理对其进行隧道传输。

我们将不胜感激。

【问题讨论】:

【参考方案1】:

一种更简单的方法,仅修补 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

【讨论】:

【参考方案2】:

我尝试了很多方法,但我发现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/

【讨论】:

【参考方案3】:

对于那些仍然需要它的人 :) 我已经使用 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>\d1,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)

【讨论】:

【参考方案4】:

正如 mkerrig 和 Denis Cornehl 在对另一个答案的评论中指出的那样,PySocks create_connection 使用来自 smtplib 的修改后的 SMTP 类工作,而无需为所有内容都打补丁。

我仍然讨厌这种实现(谁知道会与其他版本的 python 或 smtplib 发生冲突),但目前(3.8.1)有效。由于我无法在互联网上的其他地方找到任何其他有效的解决方案,这是我的尝试:

    从 smtplib.SMTP 类复制 init 和 _get_socket 函数 修改init添加proxy_addr和proxy_port 修改 _get_socket 使其返回 socks.create_connection()(相对于套接字) 将 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()

【讨论】:

【参考方案5】:

smtplib 模块不包含通过 HTTP 代理连接到 SMTP 服务器的功能。 custom class posted by 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()

【讨论】:

什么是错误 220?代码引发 IOError,接收行:HTTP/1.1 412 Precondition Failed,接收行:Cache-Control: no-cache,IOError: [Errno -1] ma: no-cache 您的代码不起作用。给出错误 - AttributeError:ProxySMTP 实例没有属性“local_hostname” 卡在Connect to proxy. Sent CONNECT. Receiving lines. :( 有什么办法解决这个问题吗?【参考方案6】:

使用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()
...

【讨论】:

正式版不支持这个。请改用this。同样的事情,只是一个叉子 2019 年更新:SocksPy 有点过时,请改用PySocks。 pip install pysocks 另请注意,尝试通过 HTTP 代理使用 SMTP 通常是不可能的,必须获得 SOCKS 代理。我个人使用 Tor,您可以阅读如何设置 Tor socks 代理 here。 PySocks 有一个非常有用的函数,称为create_connection,它使得修补 smtplib 以使用代理非常容易。只需查看smtplib 中的_get_socket 因为我遇到它并没有在这里看到它:socks.warpmodule(smtplib) 具有其他库也被修补的副作用(例如requests)。更好的方法是不使用monkeypatching,就像@mkerrig 所说,查看SMTP._get_socket 并使用socks.create_connection 嗨@mkerrig,我有一个问题,你怎么知道不能通过HTTP代理进行SMTP?谢谢! @DANIELROSASPEREZ HTTP 代理只会转发 HTTP 请求,因此流量仅限于 HTTP 协议和相关端口。我说它“通常”不可能进行 SMTP 和其他流量,因为许多代理服务器会同时处理 HTTP 和 SOCKS,但通常在购买它们或使用公共代理时,它们会列出服务器支持的协议类型。如果您不熟悉 HTTP/SOCKS 协议以及我在那里谈论的内容,您可以阅读更多关于 here 的所有内容【参考方案7】:

这个代码是我赚来的。 1.文件名不能是email.py 重命名文件名例如emailSend.py 2. 必须允许 Google 从不可靠的来源发送消息。

【讨论】:

【参考方案8】:

我昨天遇到了类似的问题,这是我为解决问题而编写的代码。它无形地允许您通过代理使用所有的 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

【讨论】:

似乎没有发送电子邮件,它只是坐在那里挂起。也尝试了几个不同的代理。 正如@Sinista 所说,您的代码不起作用,而是坐在那里挂起。我冒昧地修复了它,所以现在它对我来说工作正常。 See my answer.

以上是关于Python smtplib 代理支持的主要内容,如果未能解决你的问题,请参考以下文章

无法使用 gmail 通过 python 发送电子邮件 - smtplib.SMTPException:服务器不支持 SMTP AUTH 扩展

smtplib与email模块(实现邮件的发送)

Python3 使用smtplib和email模块发送邮件

python之发送邮件(smtplib)

python之发送邮件(smtplib)

利用Python的smtplib和email发送邮件