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 successf开发者_开发技巧ully 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.
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()
...
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
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:
- Copy the init and _get_socket functions from the smtplib.SMTP class
- Modify init to add proxy_addr, and proxy_port
- Modify _get_socket so that it returns a socks.create_connection() (vs socket)
- 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()
A much simpler approach which patches smtplib
only:
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
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.
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)
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 = '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()
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/
精彩评论