Python smtplib 代理支持

发布于 2024-10-21 02:28:43 字数 144 浏览 8 评论 0原文

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

我当前的实现如下:

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

任何帮助将不胜感激。

I would like to send email through a proxy.

My current implementation is as follows:

I connect to the smtp server with authentication. After I've successfully logged in, I send an email. It works fine but when I look at the email header I can see my host name. I would like to tunnel it through a proxy instead.

Any help will be highly appreciated.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(9

指尖上得阳光 2024-10-28 02:28:43

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

Use 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()
...
七月上 2024-10-28 02:28:43

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

I had a similar problem yesterday, this is the code I wrote to solve the problem. It invisibly allows you to use all of the smtp methods via proxy.

#!/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
伏妖词 2024-10-28 02:28:43

一种更简单的方法,仅修补 smtplib

proxy_url = urlparse('http://user:[email protected]: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

A much simpler approach which patches smtplib only:

proxy_url = urlparse('http://user:[email protected]: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
蓝戈者 2024-10-28 02:28:43

正如 mkerrig 和 Denis Cornehl 在另一个答案的评论中指出的那样,使用 smtplib 中修改后的 SMTP 类的 PySocks create_connection 可以工作,而无需为所有内容进行 Monkeypatch 套接字。

我仍然讨厌这个实现(谁知道其他版本的 python 或 smtplib 会破坏什么),但现在可以使用(3.8.1)。由于我无法在互联网上的其他地方找到任何其他有效的解决方案,因此我尝试:

  1. 从 smtplib.SMTP 类复制 init 和 _get_socket 函数
  2. 修改 init 以添加 proxy_addr 和 proxy_port
  3. 修改 _get_socket 以便它返回一个袜子。 create_connection() (与套接字)
  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()

As mkerrig and Denis Cornehl noted in a comment on another answer PySocks create_connection with a modified SMTP class from smtplib works without having to monkeypatch sockets for everything.

I still hate this implementation (who know what will break with other version of python or smtplib), but this works for now (3.8.1). Since I was unable to find any other solutions elsewhere on the internet that worked, here is my attempt:

  1. Copy the init and _get_socket functions from the smtplib.SMTP class
  2. Modify init to add proxy_addr, and proxy_port
  3. Modify _get_socket so that it returns a socks.create_connection() (vs socket)
  4. Change SMTPConnectError to smtplib.SMTPConnectError so it works

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)

And to use:

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()
看春风乍起 2024-10-28 02:28:43

这段代码是我赚来的。
1. 文件名不能是email.py 重命名文件名,例如emailSend.py
2.需要允许Google发送来自不可靠来源的消息。

This code has earned from me.
1. The file name must not be email.py Rename file name for example emailSend.py
2. It is necessary to allow Google to send messages from unreliable sources.

素年丶 2024-10-28 02:28:43

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

For those who still need it :) I've made a working solution with Python3 and 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)
白龙吟 2024-10-28 02:28:43

我不久前遇到了这个问题,这是我解决的方法。此示例使用 Tor 作为ocks5 代理。

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,其中包含您的socks5代理的 IP 和端口
  • socket.socket=socks.socksocket 分配socks.socksocket类到socket.socket属性。这有效地用socks.socksocket类替换了标准套接字类,允许所有后续套接字操作通过socks代理进行路由。
  • context.wrap_socket(socket.socket(), server_hostname=)使用提供的 SSL 上下文将套接字包装在 SSL/TLS 层中。这将创建与指定 SMTP 服务器的 SSL/TLS 连接。

I had this issue a while ago, here is how I solved it. This example uses Tor as socks5 proxy.

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() is used to set the default proxy type to SOCKS5 with the ip and port of your socks5 proxy
  • socket.socket = socks.socksocket assigns the socks.socksocket class to the socket.socket attribute. This effectively replaces the standard socket class with the socks.socksocket class, allowing all subsequent socket operations to be routed through the socks proxy.
  • context.wrap_socket(socket.socket(), server_hostname=<your_smtp_server>) wraps the socket in an SSL/TLS layer using the provided SSL context. This creates an SSL/TLS connection to the specified SMTP server.
浅紫色的梦幻 2024-10-28 02:28:43

smtplib 模块不包含通过 HTTP 代理连接到 SMTP 服务器的功能。 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 = '[email protected]'
receivers = ['[email protected]']

message = """From: From Person <[email protected]>
To: To Person <[email protected]>
Subject: SMTP e-mail test

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

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

print('Success.')
conn.close()

The smtplib module doesn't include the functionality to connect to a SMTP server through a HTTP proxy. The custom class posted by ryoh didn't work for me, apparently because my HTTP proxy receives encoded messages only. I wrote the following custom class based on ryos's code, and it worked fine. (Your mileage, however, may vary.)

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

To connect to the SMTP server, just use the class ProxySMTP instead of 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 = '[email protected]'
receivers = ['[email protected]']

message = """From: From Person <[email protected]>
To: To Person <[email protected]>
Subject: SMTP e-mail test

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

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

print('Success.')
conn.close()
温折酒 2024-10-28 02:28:43

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

I have tried many methods, but I found Nginx SMTP proxy is a better one, it is not necessary do the monkey patch, you just need to install Nginx on an internet machine connect with your private network, the Nginx Configuration is just like this.

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

ref: https://docs.nginx.com/nginx/admin-guide/mail-proxy/mail-proxy/

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文