2001-01-31 18:51:35 -04:00
|
|
|
|
#! /usr/bin/env python
|
2001-08-13 18:18:01 -03:00
|
|
|
|
"""An RFC 2821 smtp proxy.
|
2001-01-31 18:51:35 -04:00
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]]
|
2001-01-31 18:51:35 -04:00
|
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
|
|
|
|
|
--nosetuid
|
|
|
|
|
-n
|
|
|
|
|
This program generally tries to setuid `nobody', unless this flag is
|
|
|
|
|
set. The setuid call will fail if this program is not run as root (in
|
|
|
|
|
which case, use this flag).
|
|
|
|
|
|
|
|
|
|
--version
|
|
|
|
|
-V
|
|
|
|
|
Print the version number and exit.
|
|
|
|
|
|
|
|
|
|
--class classname
|
|
|
|
|
-c classname
|
2004-10-09 18:44:13 -03:00
|
|
|
|
Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by
|
2001-01-31 18:51:35 -04:00
|
|
|
|
default.
|
|
|
|
|
|
|
|
|
|
--debug
|
|
|
|
|
-d
|
|
|
|
|
Turn on debugging prints.
|
|
|
|
|
|
|
|
|
|
--help
|
|
|
|
|
-h
|
|
|
|
|
Print this message and exit.
|
|
|
|
|
|
|
|
|
|
Version: %(__version__)s
|
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
If localhost is not given then `localhost' is used, and if localport is not
|
|
|
|
|
given then 8025 is used. If remotehost is not given then `localhost' is used,
|
|
|
|
|
and if remoteport is not given, then 25 is used.
|
2001-01-31 18:51:35 -04:00
|
|
|
|
"""
|
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
# Overview:
|
|
|
|
|
#
|
|
|
|
|
# This file implements the minimal SMTP protocol as defined in RFC 821. It
|
|
|
|
|
# has a hierarchy of classes which implement the backend functionality for the
|
|
|
|
|
# smtpd. A number of classes are provided:
|
|
|
|
|
#
|
2001-04-15 10:06:04 -03:00
|
|
|
|
# SMTPServer - the base class for the backend. Raises NotImplementedError
|
2001-01-31 18:51:35 -04:00
|
|
|
|
# if you try to use it.
|
|
|
|
|
#
|
|
|
|
|
# DebuggingServer - simply prints each message it receives on stdout.
|
|
|
|
|
#
|
|
|
|
|
# PureProxy - Proxies all messages to a real smtpd which does final
|
|
|
|
|
# delivery. One known problem with this class is that it doesn't handle
|
|
|
|
|
# SMTP errors from the backend server at all. This should be fixed
|
|
|
|
|
# (contributions are welcome!).
|
|
|
|
|
#
|
|
|
|
|
# MailmanProxy - An experimental hack to work with GNU Mailman
|
|
|
|
|
# <www.list.org>. Using this server as your real incoming smtpd, your
|
|
|
|
|
# mailhost will automatically recognize and accept mail destined to Mailman
|
|
|
|
|
# lists when those lists are created. Every message not destined for a list
|
|
|
|
|
# gets forwarded to a real backend smtpd, as with PureProxy. Again, errors
|
|
|
|
|
# are not handled correctly yet.
|
|
|
|
|
#
|
|
|
|
|
# Please note that this script requires Python 2.0
|
|
|
|
|
#
|
2004-07-12 20:10:08 -03:00
|
|
|
|
# Author: Barry Warsaw <barry@python.org>
|
2001-01-31 18:51:35 -04:00
|
|
|
|
#
|
|
|
|
|
# TODO:
|
|
|
|
|
#
|
|
|
|
|
# - support mailbox delivery
|
|
|
|
|
# - alias files
|
|
|
|
|
# - ESMTP
|
|
|
|
|
# - handle error codes from the backend smtpd
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
import os
|
|
|
|
|
import errno
|
|
|
|
|
import getopt
|
|
|
|
|
import time
|
|
|
|
|
import socket
|
|
|
|
|
import asyncore
|
|
|
|
|
import asynchat
|
|
|
|
|
|
2001-02-15 18:15:14 -04:00
|
|
|
|
__all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"]
|
2001-01-31 18:51:35 -04:00
|
|
|
|
|
|
|
|
|
program = sys.argv[0]
|
|
|
|
|
__version__ = 'Python SMTP proxy version 0.2'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Devnull:
|
|
|
|
|
def write(self, msg): pass
|
|
|
|
|
def flush(self): pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DEBUGSTREAM = Devnull()
|
|
|
|
|
NEWLINE = '\n'
|
|
|
|
|
EMPTYSTRING = ''
|
2001-10-04 13:27:04 -03:00
|
|
|
|
COMMASPACE = ', '
|
2001-01-31 18:51:35 -04:00
|
|
|
|
|
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
def usage(code, msg=''):
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print(__doc__ % globals(), file=sys.stderr)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
if msg:
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print(msg, file=sys.stderr)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
sys.exit(code)
|
|
|
|
|
|
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
class SMTPChannel(asynchat.async_chat):
|
|
|
|
|
COMMAND = 0
|
|
|
|
|
DATA = 1
|
|
|
|
|
|
|
|
|
|
def __init__(self, server, conn, addr):
|
|
|
|
|
asynchat.async_chat.__init__(self, conn)
|
|
|
|
|
self.__server = server
|
|
|
|
|
self.__conn = conn
|
|
|
|
|
self.__addr = addr
|
|
|
|
|
self.__line = []
|
|
|
|
|
self.__state = self.COMMAND
|
|
|
|
|
self.__greeting = 0
|
|
|
|
|
self.__mailfrom = None
|
|
|
|
|
self.__rcpttos = []
|
|
|
|
|
self.__data = ''
|
2001-10-04 13:27:04 -03:00
|
|
|
|
self.__fqdn = socket.getfqdn()
|
2001-01-31 18:51:35 -04:00
|
|
|
|
self.__peer = conn.getpeername()
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('Peer:', repr(self.__peer), file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
self.push('220 %s %s' % (self.__fqdn, __version__))
|
|
|
|
|
self.set_terminator('\r\n')
|
|
|
|
|
|
|
|
|
|
# Overrides base class for convenience
|
|
|
|
|
def push(self, msg):
|
|
|
|
|
asynchat.async_chat.push(self, msg + '\r\n')
|
|
|
|
|
|
|
|
|
|
# Implementation of base class abstract method
|
|
|
|
|
def collect_incoming_data(self, data):
|
Merged revisions 56753-56781 via svnmerge from
svn+ssh://pythondev@svn.python.org/python/branches/p3yk
................
r56760 | neal.norwitz | 2007-08-05 18:55:39 -0700 (Sun, 05 Aug 2007) | 178 lines
Merged revisions 56477-56759 via svnmerge from
svn+ssh://pythondev@svn.python.org/python/trunk
........
r56485 | facundo.batista | 2007-07-21 17:13:00 -0700 (Sat, 21 Jul 2007) | 5 lines
Selectively enable tests for asyncore.readwrite based on the presence
of poll support in the select module (since this is the only case in
which readwrite can be called). [GSoC - Alan McIntyre]
........
r56488 | nick.coghlan | 2007-07-22 03:18:07 -0700 (Sun, 22 Jul 2007) | 1 line
Add explicit relative import tests for runpy.run_module
........
r56509 | nick.coghlan | 2007-07-23 06:41:45 -0700 (Mon, 23 Jul 2007) | 5 lines
Correctly cleanup sys.modules after executing runpy relative import
tests
Restore Python 2.4 ImportError when attempting to execute a package
(as imports cannot be guaranteed to work properly if you try it)
........
r56519 | nick.coghlan | 2007-07-24 06:07:38 -0700 (Tue, 24 Jul 2007) | 1 line
Tweak runpy test to do a better job of confirming that sys has been manipulated correctly
........
r56520 | nick.coghlan | 2007-07-24 06:58:28 -0700 (Tue, 24 Jul 2007) | 1 line
Fix an incompatibility between the -i and -m command line switches as reported on python-dev by PJE - runpy.run_module now leaves any changes it makes to the sys module intact after the function terminates
........
r56523 | nick.coghlan | 2007-07-24 07:39:23 -0700 (Tue, 24 Jul 2007) | 1 line
Try to get rid of spurious failure in test_resource on the Debian buildbots by changing the file size limit before attempting to close the file
........
r56533 | facundo.batista | 2007-07-24 14:20:42 -0700 (Tue, 24 Jul 2007) | 7 lines
New tests for basic behavior of smtplib.SMTP and
smtpd.DebuggingServer. Change to use global host & port number
variables. Modified the 'server' to take a string to send back in
order to vary test server responses. Added a test for the reaction of
smtplib.SMTP to a non-200 HELO response. [GSoC - Alan McIntyre]
........
r56538 | nick.coghlan | 2007-07-25 05:57:48 -0700 (Wed, 25 Jul 2007) | 1 line
More buildbot cleanup - let the OS assign the port for test_urllib2_localnet
........
r56539 | nick.coghlan | 2007-07-25 06:18:58 -0700 (Wed, 25 Jul 2007) | 1 line
Add a temporary diagnostic message before a strange failure on the alpha Debian buildbot
........
r56543 | martin.v.loewis | 2007-07-25 09:24:23 -0700 (Wed, 25 Jul 2007) | 2 lines
Change location of the package index to pypi.python.org/pypi
........
r56551 | georg.brandl | 2007-07-26 02:36:25 -0700 (Thu, 26 Jul 2007) | 2 lines
tabs, newlines and crs are valid XML characters.
........
r56553 | nick.coghlan | 2007-07-26 07:03:00 -0700 (Thu, 26 Jul 2007) | 1 line
Add explicit test for a misbehaving math.floor
........
r56561 | mark.hammond | 2007-07-26 21:52:32 -0700 (Thu, 26 Jul 2007) | 3 lines
In consultation with Kristjan Jonsson, only define WINVER and _WINNT_WIN32
if (a) we are building Python itself and (b) no one previously defined them
........
r56562 | mark.hammond | 2007-07-26 22:08:54 -0700 (Thu, 26 Jul 2007) | 2 lines
Correctly detect AMD64 architecture on VC2003
........
r56566 | nick.coghlan | 2007-07-27 03:36:30 -0700 (Fri, 27 Jul 2007) | 1 line
Make test_math error messages more meaningful for small discrepancies in results
........
r56588 | martin.v.loewis | 2007-07-27 11:28:22 -0700 (Fri, 27 Jul 2007) | 2 lines
Bug #978833: Close https sockets by releasing the _ssl object.
........
r56601 | martin.v.loewis | 2007-07-28 00:03:05 -0700 (Sat, 28 Jul 2007) | 3 lines
Bug #1704793: Return UTF-16 pair if unicodedata.lookup cannot
represent the result in a single character.
........
r56604 | facundo.batista | 2007-07-28 07:21:22 -0700 (Sat, 28 Jul 2007) | 9 lines
Moved all of the capture_server socket setup code into the try block
so that the event gets set if a failure occurs during server setup
(otherwise the test will block forever). Changed to let the OS assign
the server port number, and client side of test waits for port number
assignment before proceeding. The test data in DispatcherWithSendTests
is also sent in multiple send() calls instead of one to make sure this
works properly. [GSoC - Alan McIntyre]
........
r56611 | georg.brandl | 2007-07-29 01:26:10 -0700 (Sun, 29 Jul 2007) | 2 lines
Clarify PEP 343 description.
........
r56614 | georg.brandl | 2007-07-29 02:11:15 -0700 (Sun, 29 Jul 2007) | 2 lines
try-except-finally is new in 2.5.
........
r56617 | facundo.batista | 2007-07-29 07:23:08 -0700 (Sun, 29 Jul 2007) | 9 lines
Added tests for asynchat classes simple_producer & fifo, and the
find_prefix_at_end function. Check behavior of a string given as a
producer. Added tests for behavior of asynchat.async_chat when given
int, long, and None terminator arguments. Added usepoll attribute to
TestAsynchat to allow running the asynchat tests with poll support
chosen whether it's available or not (improves coverage of asyncore
code). [GSoC - Alan McIntyre]
........
r56620 | georg.brandl | 2007-07-29 10:38:35 -0700 (Sun, 29 Jul 2007) | 2 lines
Bug #1763149: use proper slice syntax in docstring.
(backport)
........
r56624 | mark.hammond | 2007-07-29 17:45:29 -0700 (Sun, 29 Jul 2007) | 4 lines
Correct use of Py_BUILD_CORE - now make sure it is defined before it is
referenced, and also fix definition of _WIN32_WINNT.
Resolves patch 1761803.
........
r56632 | facundo.batista | 2007-07-30 20:03:34 -0700 (Mon, 30 Jul 2007) | 8 lines
When running asynchat tests on OS X (darwin), the test client now
overrides asyncore.dispatcher.handle_expt to do nothing, since
select.poll gives a POLLHUP error at the completion of these tests.
Added timeout & count arguments to several asyncore.loop calls to
avoid the possibility of a test hanging up a build. [GSoC - Alan
McIntyre]
........
r56633 | nick.coghlan | 2007-07-31 06:38:01 -0700 (Tue, 31 Jul 2007) | 1 line
Eliminate RLock race condition reported in SF bug #1764059
........
r56636 | martin.v.loewis | 2007-07-31 12:57:56 -0700 (Tue, 31 Jul 2007) | 2 lines
Define _BSD_SOURCE, to get access to POSIX extensions on OpenBSD 4.1+.
........
r56653 | facundo.batista | 2007-08-01 16:18:36 -0700 (Wed, 01 Aug 2007) | 9 lines
Allow the OS to select a free port for each test server. For
DebuggingServerTests, construct SMTP objects with a localhost argument
to avoid abysmally long FQDN lookups (not relevant to items under
test) on some machines that would cause the test to fail. Moved server
setup code in the server function inside the try block to avoid the
possibility of setup failure hanging the test. Minor edits to conform
to PEP 8. [GSoC - Alan McIntyre]
........
r56681 | matthias.klose | 2007-08-02 14:33:13 -0700 (Thu, 02 Aug 2007) | 2 lines
- Allow Emacs 22 for building the documentation in info format.
........
r56689 | neal.norwitz | 2007-08-02 23:46:29 -0700 (Thu, 02 Aug 2007) | 1 line
Py_ssize_t is defined regardless of HAVE_LONG_LONG. Will backport
........
r56727 | hyeshik.chang | 2007-08-03 21:10:18 -0700 (Fri, 03 Aug 2007) | 3 lines
Fix gb18030 codec's bug that doesn't map two-byte characters on
GB18030 extension in encoding. (bug reported by Bjorn Stabell)
........
r56751 | neal.norwitz | 2007-08-04 20:23:31 -0700 (Sat, 04 Aug 2007) | 7 lines
Handle errors when generating a warning.
The value is always written to the returned pointer if getting it was
successful, even if a warning causes an error. (This probably doesn't matter
as the caller will probably discard the value.)
Will backport.
........
................
2007-08-06 20:33:07 -03:00
|
|
|
|
self.__line.append(str(data, "utf8"))
|
2001-01-31 18:51:35 -04:00
|
|
|
|
|
|
|
|
|
# Implementation of base class abstract method
|
|
|
|
|
def found_terminator(self):
|
|
|
|
|
line = EMPTYSTRING.join(self.__line)
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('Data:', repr(line), file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
self.__line = []
|
|
|
|
|
if self.__state == self.COMMAND:
|
|
|
|
|
if not line:
|
|
|
|
|
self.push('500 Error: bad syntax')
|
|
|
|
|
return
|
|
|
|
|
method = None
|
|
|
|
|
i = line.find(' ')
|
|
|
|
|
if i < 0:
|
|
|
|
|
command = line.upper()
|
|
|
|
|
arg = None
|
|
|
|
|
else:
|
|
|
|
|
command = line[:i].upper()
|
|
|
|
|
arg = line[i+1:].strip()
|
|
|
|
|
method = getattr(self, 'smtp_' + command, None)
|
|
|
|
|
if not method:
|
|
|
|
|
self.push('502 Error: command "%s" not implemented' % command)
|
|
|
|
|
return
|
|
|
|
|
method(arg)
|
|
|
|
|
return
|
|
|
|
|
else:
|
2001-03-02 02:42:34 -04:00
|
|
|
|
if self.__state != self.DATA:
|
2001-01-31 18:51:35 -04:00
|
|
|
|
self.push('451 Internal confusion')
|
|
|
|
|
return
|
|
|
|
|
# Remove extraneous carriage returns and de-transparency according
|
|
|
|
|
# to RFC 821, Section 4.5.2.
|
|
|
|
|
data = []
|
|
|
|
|
for text in line.split('\r\n'):
|
|
|
|
|
if text and text[0] == '.':
|
|
|
|
|
data.append(text[1:])
|
|
|
|
|
else:
|
|
|
|
|
data.append(text)
|
|
|
|
|
self.__data = NEWLINE.join(data)
|
|
|
|
|
status = self.__server.process_message(self.__peer,
|
|
|
|
|
self.__mailfrom,
|
|
|
|
|
self.__rcpttos,
|
|
|
|
|
self.__data)
|
|
|
|
|
self.__rcpttos = []
|
|
|
|
|
self.__mailfrom = None
|
|
|
|
|
self.__state = self.COMMAND
|
|
|
|
|
self.set_terminator('\r\n')
|
|
|
|
|
if not status:
|
|
|
|
|
self.push('250 Ok')
|
|
|
|
|
else:
|
|
|
|
|
self.push(status)
|
|
|
|
|
|
|
|
|
|
# SMTP and ESMTP commands
|
|
|
|
|
def smtp_HELO(self, arg):
|
|
|
|
|
if not arg:
|
|
|
|
|
self.push('501 Syntax: HELO hostname')
|
|
|
|
|
return
|
|
|
|
|
if self.__greeting:
|
|
|
|
|
self.push('503 Duplicate HELO/EHLO')
|
|
|
|
|
else:
|
|
|
|
|
self.__greeting = arg
|
|
|
|
|
self.push('250 %s' % self.__fqdn)
|
|
|
|
|
|
|
|
|
|
def smtp_NOOP(self, arg):
|
|
|
|
|
if arg:
|
|
|
|
|
self.push('501 Syntax: NOOP')
|
|
|
|
|
else:
|
|
|
|
|
self.push('250 Ok')
|
|
|
|
|
|
|
|
|
|
def smtp_QUIT(self, arg):
|
|
|
|
|
# args is ignored
|
|
|
|
|
self.push('221 Bye')
|
|
|
|
|
self.close_when_done()
|
|
|
|
|
|
|
|
|
|
# factored
|
|
|
|
|
def __getaddr(self, keyword, arg):
|
|
|
|
|
address = None
|
|
|
|
|
keylen = len(keyword)
|
|
|
|
|
if arg[:keylen].upper() == keyword:
|
|
|
|
|
address = arg[keylen:].strip()
|
2001-11-03 23:04:25 -04:00
|
|
|
|
if not address:
|
|
|
|
|
pass
|
|
|
|
|
elif address[0] == '<' and address[-1] == '>' and address != '<>':
|
2001-01-31 18:51:35 -04:00
|
|
|
|
# Addresses can be in the form <person@dom.com> but watch out
|
|
|
|
|
# for null address, e.g. <>
|
|
|
|
|
address = address[1:-1]
|
|
|
|
|
return address
|
|
|
|
|
|
|
|
|
|
def smtp_MAIL(self, arg):
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('===> MAIL', arg, file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
address = self.__getaddr('FROM:', arg)
|
|
|
|
|
if not address:
|
|
|
|
|
self.push('501 Syntax: MAIL FROM:<address>')
|
|
|
|
|
return
|
|
|
|
|
if self.__mailfrom:
|
|
|
|
|
self.push('503 Error: nested MAIL command')
|
|
|
|
|
return
|
|
|
|
|
self.__mailfrom = address
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('sender:', self.__mailfrom, file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
self.push('250 Ok')
|
|
|
|
|
|
|
|
|
|
def smtp_RCPT(self, arg):
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('===> RCPT', arg, file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
if not self.__mailfrom:
|
|
|
|
|
self.push('503 Error: need MAIL command')
|
|
|
|
|
return
|
|
|
|
|
address = self.__getaddr('TO:', arg)
|
|
|
|
|
if not address:
|
|
|
|
|
self.push('501 Syntax: RCPT TO: <address>')
|
|
|
|
|
return
|
|
|
|
|
self.__rcpttos.append(address)
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('recips:', self.__rcpttos, file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
self.push('250 Ok')
|
|
|
|
|
|
|
|
|
|
def smtp_RSET(self, arg):
|
|
|
|
|
if arg:
|
|
|
|
|
self.push('501 Syntax: RSET')
|
|
|
|
|
return
|
|
|
|
|
# Resets the sender, recipients, and data, but not the greeting
|
|
|
|
|
self.__mailfrom = None
|
|
|
|
|
self.__rcpttos = []
|
|
|
|
|
self.__data = ''
|
|
|
|
|
self.__state = self.COMMAND
|
|
|
|
|
self.push('250 Ok')
|
|
|
|
|
|
|
|
|
|
def smtp_DATA(self, arg):
|
|
|
|
|
if not self.__rcpttos:
|
|
|
|
|
self.push('503 Error: need RCPT command')
|
|
|
|
|
return
|
|
|
|
|
if arg:
|
|
|
|
|
self.push('501 Syntax: DATA')
|
|
|
|
|
return
|
|
|
|
|
self.__state = self.DATA
|
|
|
|
|
self.set_terminator('\r\n.\r\n')
|
|
|
|
|
self.push('354 End data with <CR><LF>.<CR><LF>')
|
|
|
|
|
|
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
class SMTPServer(asyncore.dispatcher):
|
|
|
|
|
def __init__(self, localaddr, remoteaddr):
|
|
|
|
|
self._localaddr = localaddr
|
|
|
|
|
self._remoteaddr = remoteaddr
|
|
|
|
|
asyncore.dispatcher.__init__(self)
|
|
|
|
|
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
|
|
|
# try to re-use a server port if possible
|
2001-10-09 12:46:31 -03:00
|
|
|
|
self.set_reuse_addr()
|
2001-01-31 18:51:35 -04:00
|
|
|
|
self.bind(localaddr)
|
|
|
|
|
self.listen(5)
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('%s started at %s\n\tLocal addr: %s\n\tRemote addr:%s' % (
|
2001-01-31 18:51:35 -04:00
|
|
|
|
self.__class__.__name__, time.ctime(time.time()),
|
2007-02-09 01:37:30 -04:00
|
|
|
|
localaddr, remoteaddr), file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
|
|
|
|
|
def handle_accept(self):
|
|
|
|
|
conn, addr = self.accept()
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
channel = SMTPChannel(self, conn, addr)
|
|
|
|
|
|
|
|
|
|
# API for "doing something useful with the message"
|
|
|
|
|
def process_message(self, peer, mailfrom, rcpttos, data):
|
|
|
|
|
"""Override this abstract method to handle messages from the client.
|
|
|
|
|
|
|
|
|
|
peer is a tuple containing (ipaddr, port) of the client that made the
|
|
|
|
|
socket connection to our smtp port.
|
|
|
|
|
|
|
|
|
|
mailfrom is the raw address the client claims the message is coming
|
|
|
|
|
from.
|
|
|
|
|
|
|
|
|
|
rcpttos is a list of raw addresses the client wishes to deliver the
|
|
|
|
|
message to.
|
|
|
|
|
|
|
|
|
|
data is a string containing the entire full text of the message,
|
|
|
|
|
headers (if supplied) and all. It has been `de-transparencied'
|
|
|
|
|
according to RFC 821, Section 4.5.2. In other words, a line
|
|
|
|
|
containing a `.' followed by other text has had the leading dot
|
|
|
|
|
removed.
|
|
|
|
|
|
|
|
|
|
This function should return None, for a normal `250 Ok' response;
|
|
|
|
|
otherwise it returns the desired response string in RFC 821 format.
|
|
|
|
|
|
|
|
|
|
"""
|
2001-04-15 10:06:04 -03:00
|
|
|
|
raise NotImplementedError
|
2001-01-31 18:51:35 -04:00
|
|
|
|
|
2001-02-09 16:06:00 -04:00
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
class DebuggingServer(SMTPServer):
|
|
|
|
|
# Do something with the gathered message
|
|
|
|
|
def process_message(self, peer, mailfrom, rcpttos, data):
|
|
|
|
|
inheaders = 1
|
|
|
|
|
lines = data.split('\n')
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('---------- MESSAGE FOLLOWS ----------')
|
2001-01-31 18:51:35 -04:00
|
|
|
|
for line in lines:
|
|
|
|
|
# headers first
|
|
|
|
|
if inheaders and not line:
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('X-Peer:', peer[0])
|
2001-01-31 18:51:35 -04:00
|
|
|
|
inheaders = 0
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print(line)
|
|
|
|
|
print('------------ END MESSAGE ------------')
|
2001-01-31 18:51:35 -04:00
|
|
|
|
|
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
class PureProxy(SMTPServer):
|
|
|
|
|
def process_message(self, peer, mailfrom, rcpttos, data):
|
|
|
|
|
lines = data.split('\n')
|
|
|
|
|
# Look for the last header
|
|
|
|
|
i = 0
|
|
|
|
|
for line in lines:
|
|
|
|
|
if not line:
|
|
|
|
|
break
|
|
|
|
|
i += 1
|
|
|
|
|
lines.insert(i, 'X-Peer: %s' % peer[0])
|
|
|
|
|
data = NEWLINE.join(lines)
|
|
|
|
|
refused = self._deliver(mailfrom, rcpttos, data)
|
|
|
|
|
# TBD: what to do with refused addresses?
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('we got some refusals:', refused, file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
|
|
|
|
|
def _deliver(self, mailfrom, rcpttos, data):
|
|
|
|
|
import smtplib
|
|
|
|
|
refused = {}
|
|
|
|
|
try:
|
|
|
|
|
s = smtplib.SMTP()
|
|
|
|
|
s.connect(self._remoteaddr[0], self._remoteaddr[1])
|
|
|
|
|
try:
|
|
|
|
|
refused = s.sendmail(mailfrom, rcpttos, data)
|
|
|
|
|
finally:
|
|
|
|
|
s.quit()
|
2007-01-10 12:19:56 -04:00
|
|
|
|
except smtplib.SMTPRecipientsRefused as e:
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('got SMTPRecipientsRefused', file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
refused = e.recipients
|
2007-01-10 12:19:56 -04:00
|
|
|
|
except (socket.error, smtplib.SMTPException) as e:
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('got', e.__class__, file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
# All recipients were refused. If the exception had an associated
|
|
|
|
|
# error code, use it. Otherwise,fake it with a non-triggering
|
|
|
|
|
# exception code.
|
|
|
|
|
errcode = getattr(e, 'smtp_code', -1)
|
|
|
|
|
errmsg = getattr(e, 'smtp_error', 'ignore')
|
|
|
|
|
for r in rcpttos:
|
|
|
|
|
refused[r] = (errcode, errmsg)
|
|
|
|
|
return refused
|
|
|
|
|
|
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
class MailmanProxy(PureProxy):
|
|
|
|
|
def process_message(self, peer, mailfrom, rcpttos, data):
|
2007-05-17 21:51:22 -03:00
|
|
|
|
from io import StringIO
|
2001-01-31 18:51:35 -04:00
|
|
|
|
from Mailman import Utils
|
|
|
|
|
from Mailman import Message
|
|
|
|
|
from Mailman import MailList
|
|
|
|
|
# If the message is to a Mailman mailing list, then we'll invoke the
|
|
|
|
|
# Mailman script directly, without going through the real smtpd.
|
|
|
|
|
# Otherwise we'll forward it to the local proxy for disposition.
|
|
|
|
|
listnames = []
|
|
|
|
|
for rcpt in rcpttos:
|
|
|
|
|
local = rcpt.lower().split('@')[0]
|
|
|
|
|
# We allow the following variations on the theme
|
|
|
|
|
# listname
|
|
|
|
|
# listname-admin
|
|
|
|
|
# listname-owner
|
|
|
|
|
# listname-request
|
|
|
|
|
# listname-join
|
|
|
|
|
# listname-leave
|
|
|
|
|
parts = local.split('-')
|
|
|
|
|
if len(parts) > 2:
|
|
|
|
|
continue
|
|
|
|
|
listname = parts[0]
|
|
|
|
|
if len(parts) == 2:
|
|
|
|
|
command = parts[1]
|
|
|
|
|
else:
|
|
|
|
|
command = ''
|
|
|
|
|
if not Utils.list_exists(listname) or command not in (
|
|
|
|
|
'', 'admin', 'owner', 'request', 'join', 'leave'):
|
|
|
|
|
continue
|
|
|
|
|
listnames.append((rcpt, listname, command))
|
|
|
|
|
# Remove all list recipients from rcpttos and forward what we're not
|
|
|
|
|
# going to take care of ourselves. Linear removal should be fine
|
|
|
|
|
# since we don't expect a large number of recipients.
|
|
|
|
|
for rcpt, listname, command in listnames:
|
|
|
|
|
rcpttos.remove(rcpt)
|
2001-02-09 16:06:00 -04:00
|
|
|
|
# If there's any non-list destined recipients left,
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('forwarding recips:', ' '.join(rcpttos), file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
if rcpttos:
|
|
|
|
|
refused = self._deliver(mailfrom, rcpttos, data)
|
|
|
|
|
# TBD: what to do with refused addresses?
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('we got refusals:', refused, file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
# Now deliver directly to the list commands
|
|
|
|
|
mlists = {}
|
|
|
|
|
s = StringIO(data)
|
|
|
|
|
msg = Message.Message(s)
|
|
|
|
|
# These headers are required for the proper execution of Mailman. All
|
|
|
|
|
# MTAs in existance seem to add these if the original message doesn't
|
|
|
|
|
# have them.
|
|
|
|
|
if not msg.getheader('from'):
|
|
|
|
|
msg['From'] = mailfrom
|
|
|
|
|
if not msg.getheader('date'):
|
|
|
|
|
msg['Date'] = time.ctime(time.time())
|
|
|
|
|
for rcpt, listname, command in listnames:
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('sending message to', rcpt, file=DEBUGSTREAM)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
mlist = mlists.get(listname)
|
|
|
|
|
if not mlist:
|
|
|
|
|
mlist = MailList.MailList(listname, lock=0)
|
|
|
|
|
mlists[listname] = mlist
|
|
|
|
|
# dispatch on the type of command
|
|
|
|
|
if command == '':
|
|
|
|
|
# post
|
|
|
|
|
msg.Enqueue(mlist, tolist=1)
|
|
|
|
|
elif command == 'admin':
|
|
|
|
|
msg.Enqueue(mlist, toadmin=1)
|
|
|
|
|
elif command == 'owner':
|
|
|
|
|
msg.Enqueue(mlist, toowner=1)
|
|
|
|
|
elif command == 'request':
|
|
|
|
|
msg.Enqueue(mlist, torequest=1)
|
|
|
|
|
elif command in ('join', 'leave'):
|
|
|
|
|
# TBD: this is a hack!
|
|
|
|
|
if command == 'join':
|
|
|
|
|
msg['Subject'] = 'subscribe'
|
|
|
|
|
else:
|
|
|
|
|
msg['Subject'] = 'unsubscribe'
|
|
|
|
|
msg.Enqueue(mlist, torequest=1)
|
|
|
|
|
|
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
class Options:
|
|
|
|
|
setuid = 1
|
|
|
|
|
classname = 'PureProxy'
|
|
|
|
|
|
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
def parseargs():
|
|
|
|
|
global DEBUGSTREAM
|
|
|
|
|
try:
|
|
|
|
|
opts, args = getopt.getopt(
|
|
|
|
|
sys.argv[1:], 'nVhc:d',
|
|
|
|
|
['class=', 'nosetuid', 'version', 'help', 'debug'])
|
2007-01-10 12:19:56 -04:00
|
|
|
|
except getopt.error as e:
|
2001-01-31 18:51:35 -04:00
|
|
|
|
usage(1, e)
|
|
|
|
|
|
|
|
|
|
options = Options()
|
|
|
|
|
for opt, arg in opts:
|
|
|
|
|
if opt in ('-h', '--help'):
|
|
|
|
|
usage(0)
|
|
|
|
|
elif opt in ('-V', '--version'):
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print(__version__, file=sys.stderr)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
sys.exit(0)
|
|
|
|
|
elif opt in ('-n', '--nosetuid'):
|
|
|
|
|
options.setuid = 0
|
|
|
|
|
elif opt in ('-c', '--class'):
|
|
|
|
|
options.classname = arg
|
|
|
|
|
elif opt in ('-d', '--debug'):
|
|
|
|
|
DEBUGSTREAM = sys.stderr
|
|
|
|
|
|
|
|
|
|
# parse the rest of the arguments
|
2001-10-04 13:27:04 -03:00
|
|
|
|
if len(args) < 1:
|
|
|
|
|
localspec = 'localhost:8025'
|
|
|
|
|
remotespec = 'localhost:25'
|
|
|
|
|
elif len(args) < 2:
|
2001-01-31 18:51:35 -04:00
|
|
|
|
localspec = args[0]
|
2001-10-04 13:27:04 -03:00
|
|
|
|
remotespec = 'localhost:25'
|
2001-11-03 23:04:25 -04:00
|
|
|
|
elif len(args) < 3:
|
|
|
|
|
localspec = args[0]
|
|
|
|
|
remotespec = args[1]
|
2001-10-04 13:27:04 -03:00
|
|
|
|
else:
|
|
|
|
|
usage(1, 'Invalid arguments: %s' % COMMASPACE.join(args))
|
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
# split into host/port pairs
|
|
|
|
|
i = localspec.find(':')
|
|
|
|
|
if i < 0:
|
2001-10-04 13:27:04 -03:00
|
|
|
|
usage(1, 'Bad local spec: %s' % localspec)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
options.localhost = localspec[:i]
|
|
|
|
|
try:
|
|
|
|
|
options.localport = int(localspec[i+1:])
|
|
|
|
|
except ValueError:
|
2001-10-04 13:27:04 -03:00
|
|
|
|
usage(1, 'Bad local port: %s' % localspec)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
i = remotespec.find(':')
|
|
|
|
|
if i < 0:
|
2001-10-04 13:27:04 -03:00
|
|
|
|
usage(1, 'Bad remote spec: %s' % remotespec)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
options.remotehost = remotespec[:i]
|
|
|
|
|
try:
|
|
|
|
|
options.remoteport = int(remotespec[i+1:])
|
|
|
|
|
except ValueError:
|
2001-10-04 13:27:04 -03:00
|
|
|
|
usage(1, 'Bad remote port: %s' % remotespec)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
return options
|
|
|
|
|
|
|
|
|
|
|
2001-10-04 13:27:04 -03:00
|
|
|
|
|
2001-01-31 18:51:35 -04:00
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
options = parseargs()
|
|
|
|
|
# Become nobody
|
|
|
|
|
if options.setuid:
|
|
|
|
|
try:
|
|
|
|
|
import pwd
|
|
|
|
|
except ImportError:
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('Cannot import module "pwd"; try running with -n option.', file=sys.stderr)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
sys.exit(1)
|
|
|
|
|
nobody = pwd.getpwnam('nobody')[2]
|
|
|
|
|
try:
|
|
|
|
|
os.setuid(nobody)
|
2007-01-10 12:19:56 -04:00
|
|
|
|
except OSError as e:
|
2001-03-02 02:42:34 -04:00
|
|
|
|
if e.errno != errno.EPERM: raise
|
2007-02-09 01:37:30 -04:00
|
|
|
|
print('Cannot setuid "nobody"; try running with -n option.', file=sys.stderr)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
sys.exit(1)
|
2004-06-26 16:18:49 -03:00
|
|
|
|
classname = options.classname
|
|
|
|
|
if "." in classname:
|
|
|
|
|
lastdot = classname.rfind(".")
|
|
|
|
|
mod = __import__(classname[:lastdot], globals(), locals(), [""])
|
|
|
|
|
classname = classname[lastdot+1:]
|
|
|
|
|
else:
|
|
|
|
|
import __main__ as mod
|
|
|
|
|
class_ = getattr(mod, classname)
|
2001-01-31 18:51:35 -04:00
|
|
|
|
proxy = class_((options.localhost, options.localport),
|
|
|
|
|
(options.remotehost, options.remoteport))
|
|
|
|
|
try:
|
|
|
|
|
asyncore.loop()
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
pass
|