プロキシ経由でメールを送信したいと考えています。
私の現在の実装は次のとおりです。
認証を使用して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 が別の回答のコメントで指摘したように、smtplib から変更された SMTP クラスを使用した PySocks create_connection は、すべてのソケットにモンキーパッチを適用する必要なく機能します。
私はまだこの実装が嫌いです (他のバージョンの python または smtplib で何が壊れるかを知っている人はいます) が、これは今のところ機能します (3.8.1)。インターネット上の他の場所で機能する他のソリューションを見つけることができなかったので、ここに私の試みがあります:
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