先上结论:改写IMAP4_SSL 这个类,把网络连接由于 socket 改为 socks 即可。 如果需要发邮件,那么用同样的方法改写SMTP_SSL这个类即可。

# -*-coding: utf-8 -*-
from socks import create_connection, PROXY_TYPE_SOCKS4, PROXY_TYPE_SOCKS5, PROXY_TYPE_HTTP
from imaplib import IMAP4_SSL, IMAP4_PORT, IMAP4_SSL_PORT
from smtplib import SMTP_SSL
import socket


# 收邮件用这个
# 为什么取名SocksIMAP4SSL 而 不叫 ProxyIMAP4SSL? 因为socks库的本质是对socket库的扩展,增加了通过Proxy进行网络通信的支持。 (python自带的socket库不支持proxy)
class SocksIMAP4SSL(IMAP4_SSL):

    #  __init__ 除了新增三个参数外,其他没有任何改变<------ 执行一次 IMAP4_SSL.__init__ () 把 IMAP4_SSL.__init__ () 的全部继承过来
    def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None,
                 certfile=None, ssl_context=None, proxy_addr=None,
                 proxy_port=None, rdns=True):
        self.proxy_addr = proxy_addr
        self.proxy_port = proxy_port
        self.rdns = rdns

        IMAP4_SSL.__init__(self, host=host, port=port, keyfile=keyfile, certfile=certfile, ssl_context=ssl_context)

    # 用 socks.create_connection 替换 socket.create_connection。 一个是socket,一个是socks,一个字母的差别!
    def _create_socket(self):
        sock = create_connection((self.host, self.port), proxy_type=PROXY_TYPE_HTTP, proxy_addr=self.proxy_addr,
                                 proxy_port=self.proxy_port)
        return self.ssl_context.wrap_socket(sock, server_hostname=self.host)

# 发邮件用这个
class SocksSMTP_SSL(SMTP_SSL):

    def __init__(self, host='', port=465, local_hostname=None, keyfile=None,
                 certfile=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
                 source_address=None, context=None, proxy_addr=None, proxy_port=None):
        self.proxy_addr = proxy_addr
        self.proxy_port = proxy_port
        super().__init__(host, port, local_hostname, keyfile, certfile, timeout, source_address, context)

    # 如果有proxy,就用 socks.create_connection 替换 socket.create_connection,否则通过super()用回官方原来的即可。
    def _get_socket(self, host, port, timeout):
        if self.proxy_addr and self.proxy_port:
            sock = create_connection((host, port), timeout, proxy_type=PROXY_TYPE_HTTP, proxy_addr=self.proxy_addr, proxy_port=self.proxy_port)
            return self.context.wrap_socket(sock, server_hostname=host)
        else:
            return super()._get_socket(host, port, timeout)

以上代码已经直接可用。

------------------------------->

另备注:

网易邮箱需额外添加的内容:

收邮件,需要添加:imaplib.Commands['ID'] = ('AUTH') 

import imaplib
 
# 添加缺失的命令
imaplib.Commands['ID'] = ('AUTH')
 
conn = imaplib.IMAP4_SSL(port = '993',host = 'imap.163.com')
conn.login('XXXX@163.com','HJKHSHDFIWRNKJHI')
 
# 上传客户端身份信息
args = ("name","XXXX","contact","XXXX@163.com","version","1.0.0","vendor","myclient")
typ, dat = conn._simple_command('ID', '("' + '" "'.join(args) + '")')
print(conn._untagged_response(typ, dat, 'ID'))
 
status, msgs = conn.select()

 通过126发邮件,需要添加:mail_client.set_debuglevel(1)  # 解决 500 Error: bad syntax

s = SocksSMTP_SSL(smtp_server, smtp_server_port,proxy_addr=proxy_addr, proxy_port=proxy_port)  # 通过ip代理连接smtp服务器
s.set_debuglevel(1)  # 解决 500 Error: bad syntax
s.ehlo(smtp_server)
s.login(sender_address, pwd)  # 登录邮箱
s.sendmail(sender_address, all_recipients, mail_msg.as_string())  # 发送邮件
s.quit()

----------------------------------->以下内仅是之前踩坑摸索的过程记录。

对于网易邮箱的 500 Error: bad syntax 的解决方案,还有 修改主机名为ip (fqdn = socket.getfqdn())或修改dns名等方法[网络属性-->双击“Internet 协议版本 4”->高级->DNS->更改“此链接的DNS后缀”->随便填几个字母] 我没有逐一验证。GPT给的建议是加time.sleep

        s = SocksSMTP_SSL(smtp_server, smtp_server_port,proxy_addr=proxy_addr, proxy_port=proxy_port)  # 通过ip代理连接smtp服务器
        # s.set_debuglevel(1)  # 解决 500 Error: bad syntax
        time.sleep(1)
        s.ehlo(smtp_server)
        time.sleep(1)
        s.login(sender_address, pwd)  # 登录邮箱
        time.sleep(1)
        s.sendmail(sender_address, all_recipients, mail_msg.as_string())  # 发送邮件
        s.quit()

python通过代理访问网络的简单直接方法:

在程序开头插入以下代码

import socket
import socks
socks.set_default_proxy(socks.SOCKS5, "代理服务器IP", 代理服务器端口)
socket.socket = socks.socksocket

其原理是改重写了socket.socket这个类,使任何通过socket.socket访问网络的对象都通过socks.socksocket来访问网络,

这里要注意,使用socks时要根据代理类型设置参数,类型支持3种:socks.SOCKS5 / socks.SOCKS4 / socks.HTTP

例如以下代码显示实际IP。

from urllib import request

response = request.urlopen('http://httpbin.org/ip')
str1 = response.read().decode()
print(str1)

以下代码则显示代理IP:(request对象是通过socket.socket访问网络的)

from urllib import request

import socket
import socks
socks.set_default_proxy(socks.SOCKS5, "222.129.38.21", 57114)
socket.socket = socks.socksocket

response = request.urlopen('http://httpbin.org/ip')
str1 = response.read().decode()
print(str1)

但我不想全局使用代理,只想imaplib使用代理。那就要找到imaplib中创建网络连接的那段代码,并重写即可。

IMAP4_SSL中创建网络链接的源代码如下:

class IMAP4_SSL(IMAP4):

    def _create_socket(self):

        host = None if not self.host else self.host
        sys.audit("imaplib.open", self, self.host, self.port)
        sock = socket.create_connection((host, self.port))        
        return self.ssl_context.wrap_socket(sock,
                                            server_hostname=self.host)

 创建一个IMAP4_SSL的子类,并重写其_create_socket方法:

class SocksIMAP4SSL(IMAP4_SSL):

    def _create_socket(self):

        p_addr = '222.129.38.21'
        p_port = 57114

        sock = socks.create_connection((self.host, self.port), proxy_type=PROXY_TYPE_HTTP, proxy_addr=p_addr, proxy_port=p_port)
        return self.ssl_context.wrap_socket(sock, server_hostname=self.host)


#
# 关于 return self.ssl_context.wrap_socket(sock, server_hostname=self.host) 见到一些例子加入 ssl.HAS_SNI 判断,这里需要吗?暂时未清楚。如下:
#       server_hostname = self.host if ssl.HAS_SNI else None
#       return self.ssl_context.wrap_socket(sock, server_hostname=server_hostname)

-----------------------------------------

知道以上原理之前,我的摸索过程是这样的:在网上找了一轮代码,试过可行的是这段:

Python IMAP proxy connection - Stack Overflow (CSDN上也有人贴出 Python IMAP 设置代理_python imaplib 使用代理-CSDN博客

import ssl, time

from socks import create_connection
from socks import PROXY_TYPE_SOCKS4
from socks import PROXY_TYPE_SOCKS5
from socks import PROXY_TYPE_HTTP

from imaplib import IMAP4
from imaplib import IMAP4_PORT
from imaplib import IMAP4_SSL_PORT
from filter import get_user_pass

__author__ = "sstevan"
__license__ = "GPLv3"
__version__ = "0.1"


class SocksIMAP4(IMAP4):
    """
    IMAP service trough SOCKS proxy. PySocks module required.
    """

    PROXY_TYPES = {"socks4": PROXY_TYPE_SOCKS4,
                   "socks5": PROXY_TYPE_SOCKS5,
                   "http": PROXY_TYPE_HTTP}

    def __init__(self, host, port=IMAP4_PORT, proxy_addr=None, proxy_port=None,
                 rdns=True, username=None, password=None, proxy_type="socks5"):

        self.proxy_addr = proxy_addr
        self.proxy_port = proxy_port
        self.rdns = rdns
        self.username = username
        self.password = password
        self.proxy_type = SocksIMAP4.PROXY_TYPES[proxy_type.lower()]

        IMAP4.__init__(self, host, port)

    def _create_socket(self):
        return create_connection((self.host, self.port), proxy_type=self.proxy_type, proxy_addr=self.proxy_addr,
                                 proxy_port=self.proxy_port, proxy_rdns=self.rdns, proxy_username=self.username,
                                 proxy_password=self.password)


class SocksIMAP4SSL(SocksIMAP4):

    def __init__(self, host='', port=IMAP4_SSL_PORT, keyfile=None, certfile=None, ssl_context=None, proxy_addr=None,
                 proxy_port=None, rdns=True, username=None, password=None, proxy_type="socks5"):

        if ssl_context is not None and keyfile is not None:
                raise ValueError("ssl_context and keyfile arguments are mutually "
                                 "exclusive")
        if ssl_context is not None and certfile is not None:
            raise ValueError("ssl_context and certfile arguments are mutually "
                             "exclusive")

        self.keyfile = keyfile
        self.certfile = certfile
        if ssl_context is None:
            ssl_context = ssl._create_stdlib_context(certfile=certfile,
                                                     keyfile=keyfile)
        self.ssl_context = ssl_context

        SocksIMAP4.__init__(self, host, port, proxy_addr=proxy_addr, proxy_port=proxy_port,
                            rdns=rdns, username=username, password=password, proxy_type=proxy_type)

    def _create_socket(self):
        sock = SocksIMAP4._create_socket(self)
        server_hostname = self.host if ssl.HAS_SNI else None
        return self.ssl_context.wrap_socket(sock, server_hostname=server_hostname)

    def open(self, host='', port=IMAP4_PORT):
        SocksIMAP4.open(self, host, port)

def connect_proxy(imap_server, imap_port, proxy_addr, proxy_port, proxy_type, email, password):
    mailbox = SocksIMAP4SSL(host=imap_server, port=imap_port,
                            proxy_addr=proxy_addr, proxy_port=proxy_port, proxy_type=proxy_type)
    try:
        mailbox.login(email, password)
        print("We are here")
        print("OK ",)
    except Exception as e:
        print(e)
        return False
    print(mailbox.state)
    mailbox.logout()
    return True


if __name__ == "__main__":
    imap_server = "imap.rambler.ru"
    imap_port = 993

    proxy_addr = "188.120.224.172"
    proxy_port = 59923
    proxy_type = "socks5"
    email, password = get_user_pass("pm@mail11.rambler.ru:11")
    if email is not None:
        resp = connect_proxy(imap_server, imap_port, proxy_addr, proxy_port, proxy_type, email, password)
        #resp = connect(email, password, "smtp.rambler.ru")
    time.sleep(1)

但感觉这段有点把问题搞复杂了(我仅用到IMAP4_SSL,非SSL的不需要)。有没有直接对IMAP4_SSL设置代理的呢?以下这段似乎更合适:python - How can I fetch emails via POP or IMAP through a proxy? - Stack Overflow  (下面称其为B方案)

import ssl

class SocksIMAP4SSL(IMAP4_SSL):
    def open(self, host, port=IMAP4_SSL_PORT):
        self.host = host
        self.port = port
        #actual privoxy default setting, but as said, you may want to parameterize it
        self.sock = create_connection((host, port), PROXY_TYPE_HTTP, "127.0.0.1", 8118)
        self.sslobj = ssl.wrap_socket(self.sock, self.keyfile, self.certfile)
        self.file = self.sslobj.makefile('rb')

但调试了很久都不成功。

既然B方案是 IMAP4_SSL的子类,只修改了其中的open方法,那么追踪一下原生IMAP4_SSL的open方法的源代码,看看A方案、B方案、原生代码三者之间差别吧,

查看IMAP4_SSL的源代码及查看官方文档得知,IMAP4_SSL是IMAP4的子类,(文档链接)This is a subclass derived from IMAP4 that connects over an SSL encrypted socket。

在官方源代码上发现,A方案实际上是把IMAP4_SSL的原生源代码拷贝了出来,怪不得A方案没有调用IMAP4_SSL啦。

进一步对比跟踪代码,看到A方案也仅仅是修改了 IMAP4 的 _create_socket() 方法 [ 在open()中调用了_create_socket() ]。而_create_socket()中使用了  socket.create_connection,发现A方案跟IMAP4原生代码最核心的区别就是socket.create_connection()传入的参数不一样。

同样是调用socket.create_connection(),原生IMAP4只传入了目标地(host+port)这个参数,A方案传入的参数加了代理ip、代理端口两个参数,

原生:

class IMAP4:

    def _create_socket(self):
        host = None if not self.host else self.host
        sys.audit("imaplib.open", self, self.host, self.port)
        return socket.create_connection((host, self.port))

    def open(self, host='', port=IMAP4_PORT):
        """Setup connection to remote server on "host:port"
            (default: localhost:standard IMAP4 port).
        This connection will be used by the routines:
            read, readline, send, shutdown.
        """
        self.host = host
        self.port = port
        self.sock = self._create_socket()
        self.file = self.sock.makefile('rb')



class IMAP4_SSL(IMAP4):   

    def _create_socket(self):
        sock = IMAP4._create_socket(self)
        return self.ssl_context.wrap_socket(sock,
                                            server_hostname=self.host)

    def open(self, host='', port=IMAP4_SSL_PORT):
        IMAP4.open(self, host, port) 

        #注意,父类的self.port默认值是IMAP4_PORT(143),这里调用一下父类,self.port默认值就变成IMAP4_SSL_PORT了(993),所以重写open()这段不能删除

A方案:

class SocksIMAP4(IMAP4):

    def _create_socket(self):
        return create_connection((self.host, self.port), proxy_type=self.proxy_type, proxy_addr=self.proxy_addr,
                                 proxy_port=self.proxy_port, proxy_rdns=self.rdns, proxy_username=self.username,
                                 proxy_password=self.password)






class SocksIMAP4SSL(SocksIMAP4):

    def _create_socket(self):
        sock = SocksIMAP4._create_socket(self)
        server_hostname = self.host if ssl.HAS_SNI else None
        return self.ssl_context.wrap_socket(sock, server_hostname=server_hostname)

    def open(self, host='', port=IMAP4_PORT):
        SocksIMAP4.open(self, host, port)

那原生的socket.create_connection()  是怎么用的呢,有些神马玩法呢?

官方文档如下:socket --- 底层网络接口 — Python 3.9.19 文档

这里有一些示例 :Python socket.create_connection() Examples

Python Examples of socket.create_connection   

发现官方的 socket.create_connection 并不支持那么多参数啊,再仔细跟踪,IMAP4使用的是socket.create_connection,而A方案使用的是另外一个类,socks.create_connection

一个是socket,一个是socks,一个字母的差别!!

恩,再查一下socks怎么玩吧

socks的说明文档:http://socksipy.sourceforge.net/readme.txt

最后发现三者差异为:

#方案A用 和IMAP4_SSL都是用 ssl._create_stdlib_context(certfile=certfile,keyfile=keyfile).wrap_socket(sock, server_hostname=server_hostname) ,方案B用 ssl.wrap_socket(self.sock, self.keyfile, self.certfile)

他们相同吗?不知道,不想继续折腾了,放弃调试方案B,参考方案A直接重写IMAP4_SSL的_create_socket(),运行登录成功!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐