qmail itself supports much more complex mappings than these packages make use of - for example, one can mix/match system and virtual users completely arbitrarily (even within a virtual domain).
Here's a patch against TMDA-1.0.2 that provides complete support for qmail virtual domains. Instead of requiring the user ID on the command-line (vpopmail) or parsing just the /var/qmail/control/virtualdomains file (vmailmgr) the code will now parse the virtualdomains file *and* /var/qmail/users/assign file to work out which username to use for delivery.
For users with non-standard locations of these conf files, I added the -z or --vusersassign-path option to change the location of users/assign (-v or --vdomains-path was already present for the other file)
One will have to run tmda-ofmipd as root with this patch, so it can switch to the correct user for config file access, after authentication.
Note that this patch includes a previous patch I published, which allows tmda-ofmipd to run under a combination of tcpserver and stunnel, so that it logs correct remote IPs in this scenario. This new functionality is invoked using the -1 option to tmda-ofmipd (in which case, it doesn't run as a daemon, doesn't listen on a server socket, and exits after processing a single SMTP session). Without -1, it should behave as before.
It should be pretty simple to hack out the "single-session" changes from tmda-ofmipd if you're handy with Python. Take all the changes to Util.py, and just the few lines relating to vdomain stuff from tmda-ofmipd - ignore most of the re-working of the SMTP classes...
-- Stephen Warren, Software Engineer, Parama Networks, San Jose, CA [EMAIL PROTECTED] http://www.wwwdotorg.org/
Index: TMDA/Util.py
===================================================================
RCS file: /cvsroot/tmda/tmda/TMDA/Util.py,v
retrieving revision 1.110.2.4
diff -u -r1.110.2.4 Util.py
--- TMDA/Util.py 18 Feb 2004 21:05:56 -0000 1.110.2.4
+++ TMDA/Util.py 20 Jun 2004 01:40:21 -0000
@@ -128,41 +128,100 @@
return statinfo[stat.ST_UID]
-def getvdomainprepend(address, vdomainsfile):
+def getvdomainuser(address, vdomainsfile, vusersassignfile):
ret_prepend = ''
- if os.path.exists(vdomainsfile):
- fp = open(vdomainsfile, 'r')
- # Parse the virtualdomains control file; see qmail-send(8) for
- # syntax rules. All this because qmail doesn't store the original
- # envelope recipient in the environment.
- u, d = address.split('@', 1)
- ousername = u.lower()
- odomain = d.lower()
- for line in fp.readlines():
- vdomain_match = 0
- line = line.strip().lower()
- # Comment or blank line?
- if line == '' or line[0] in '#':
- continue
- vdomain, prepend = line.split(':', 1)
- # domain:prepend
- if vdomain == odomain:
- vdomain_match = 1
- # .domain:prepend (wildcard)
- elif vdomain[:1] == '.' and odomain.find(vdomain) != -1:
- vdomain_match = 1
- # [EMAIL PROTECTED]:prepend
- else:
- try:
- if vdomain.split('@', 1)[1] == odomain:
- vdomain_match = 1
- except IndexError:
- pass
- if vdomain_match:
- ret_prepend = prepend
+
+ address = address.lower()
+ ousername, odomain = address.split('@', 1)
+
+ fp = open(vdomainsfile, 'r')
+ # Parse the virtualdomains control file; see qmail-send(8) for
+ # syntax rules. All this because qmail doesn't store the original
+ # envelope recipient in the environment.
+ for line in fp.readlines():
+ vdomain_match = 0
+
+ line = line.strip().lower()
+
+ # Comment or blank line?
+ if (line == '') or (line[0] in '#'):
+ continue
+ #
+
+ vdomain, prepend = line.split(':', 1)
+
+ # :prepend (catchall)
+ if vdomain == '':
+ vdomain_match = 1
+ # [EMAIL PROTECTED] (virtual user)
+ elif '@' in vdomain:
+ vdomain_match = (vdomain == address)
+ # domain:prepend (virtual domain)
+ elif vdomain == odomain:
+ vdomain_match = 1
+ # .domain:prepend (virtual domain wildcard)
+ elif vdomain[:1] == '.' and odomain.endswith(vdomain):
+ vdomain_match = 1
+
+ if vdomain_match:
+ ret_prepend = prepend
+ break
+ #
+ #
+
+ fp.close()
+
+ vuser = '-'.join([ret_prepend, ousername])
+
+ longest_wild = ''
+ matching_user = ''
+
+ fp = open(vusersassignfile, 'r')
+ # Parse users/assign control file; see qmail-users(5) for syntax.
+ for line in fp.readlines():
+ line = line.strip().lower()
+
+ # Comment or blank line?
+ if (line == '') or (line[0] in '#'):
+ continue
+ # End of file marker?
+ if (line == '.'):
+ break
+ #
+
+ matcher, user, rest = line.split(':', 2)
+
+ line_type = matcher[:1]
+ wild = matcher[1:]
+
+ # =vuser:... (exact match)
+ if line_type in '=':
+ if wild == vuser:
+ matching_user = user
break
- fp.close()
- return ret_prepend
+ #
+ # +wild:... (wildcard)
+ if line_type in '+':
+ if vuser.startswith(wild):
+ if len(wild) > len(longest_wild):
+ longest_wild = wild
+ matching_user = user
+ #
+ #
+ # Bad line
+ else:
+ raise "Invalid qmail users/assign format"
+ #
+ #
+
+ fp.close()
+
+ if matching_user == '':
+ raise "User not in qmail configuration"
+ #
+
+ return matching_user
+#
def getvuserhomedir(user, domain, script):
Index: bin/tmda-ofmipd
===================================================================
RCS file: /cvsroot/tmda/tmda/bin/tmda-ofmipd,v
retrieving revision 1.41
diff -u -r1.41 tmda-ofmipd
--- bin/tmda-ofmipd 13 Nov 2003 19:28:08 -0000 1.41
+++ bin/tmda-ofmipd 20 Jun 2004 01:40:21 -0000
@@ -159,12 +159,25 @@
domains using the VMailMgr add-on. It implies that you will also set
the --vhome-script parameter above.
+ -z
+ --vusersassign-path <path_to_qmails_usersassign_file>
+ Full pathname to qmail's users/assign file.
+ The default for both qmail and tmda-ofmipd is:
+ /var/qmail/usrs/assign.
+
+ See -v for more details.
+
-t <script>
--throttle-script <script>
Full pathname of a script which can meter how much mail any user sends.
The script is passed a login name whenever a user tries to send mail.
If the script returns a 0, the message is allowed. For any other
- value, the message is rejected."""
+ value, the message is rejected.
+
+ -1
+ --one-session
+ Don't bind to a port and accept new connections -
+ Process a single SMTP session (e.g. when started from tcpserver)."""
import getopt
import os
@@ -196,6 +209,7 @@
authprog = None
fallback = 0
foreground = None
+one_session = 0
remoteauth = { 'proto': None,
'host': 'localhost',
'port': None,
@@ -212,6 +226,7 @@
connections = 20
vhomescript = None
vdomainspath = '/var/qmail/control/virtualdomains'
+vusersassignpath = '/var/qmail/users/assign'
throttlescript = None
if os.getuid() == 0:
@@ -261,7 +276,7 @@
try:
opts, args = getopt.getopt(sys.argv[1:],
- 'p:u:a:R:A:Fc:C:dVhfbS:v:t:', ['proxyport=',
+ 'p:u:a:R:A:Fc:C:dVhfbS:v:z:t:1', ['proxyport=',
'username=',
'authfile=',
'remoteauth=',
@@ -276,7 +291,9 @@
'background',
'vhome-script=',
'vdomains-path=',
- 'throttle-script='])
+ 'vusersassign-path=',
+ 'throttle-script=',
+ 'one-session'])
except getopt.error, msg:
usage(1, msg)
@@ -343,8 +360,12 @@
vhomescript = arg
elif opt in ('-v', '--vdomains-path'):
vdomainspath = arg
+ elif opt in ('-z', '--vusersassign-path'):
+ vusersassignpath = arg
elif opt in ('-t', '--throttle-script'):
throttlescript = arg
+ elif opt in ('-1', '--one-session'):
+ one_session = 1
if vhomescript and configdir:
msg = "WARNING: --vhome-script and --config-dir are incompatible." + \
@@ -590,8 +611,9 @@
DATA = 1
AUTH = 2
- def __init__(self, server, conn, addr):
+ def __init__(self, server, conn):
asynchat.async_chat.__init__(self, conn)
+
# SMTP AUTH
self.__smtpauth = 0
self.__auth_resp1 = None
@@ -608,7 +630,6 @@
int(time.time()), FQDN)
self.__server = server
self.__conn = conn
- self.__addr = addr
self.__line = []
self.__state = self.COMMAND
#self.__greeting = 0
@@ -616,11 +637,41 @@
self.__rcpttos = []
self.__data = ''
self.__fqdn = FQDN
- self.__peer = conn.getpeername()
- self.__peerip = self.__peer[0]
- self.__peername = socket.getfqdn(self.__peerip)
- self.__sockip = conn.getsockname()[0]
- print >> DEBUGSTREAM, 'Peer:', repr(self.__peer)
+
+ # If we're running under tcpserver, then it sets up a bunch of
+ # environment variables that give socket address information.
+ # We always use this, rather than e.g. calling getsockname on
+ # conn, because the tcpserver socket might not be passed directly
+ # to tmda-ofmipd. For example, stunnel might terminate the socket,
+ # decrypt the data and send it here over a pipe...
+ if os.environ.has_key('TCPLOCALIP'):
+ self.__peerip = os.environ['TCPREMOTEIP']
+ if os.environ.has_key('TCPREMOTEHOST'):
+ self.__peername = os.environ['TCPREMOTEHOST']
+ else:
+ self.__peername = socket.getfqdn(self.__peerip)
+ self.__peerport = os.environ['TCPREMOTEPORT']
+ self.__peer = (self.__peerip, self.__peerport)
+ self._localip = os.environ['TCPLOCALIP']
+ if os.environ.has_key('TCPLOCALHOST'):
+ self._localname = os.environ['TCPLOCALHOST']
+ else:
+ self._localname = socket.getfqdn(self._localip)
+ self._localport = os.environ['TCPLOCALPORT']
+ self._local = (self._localip, self._localport)
+ else:
+ self.__peer = conn.getpeername()
+ self.__peerip = self.__peer[0]
+ self.__peername = socket.getfqdn(self.__peerip)
+ self.__peerport = self.__peer[1]
+ self._local= conn.getsockname()
+ self._localip = self._local[0]
+ self._localname = socket.getfqdn(self._localip)
+ self._localport = self._local[1]
+
+ print >> DEBUGSTREAM, 'Incoming connection from %s' % repr(self.__peer)
+ print >> DEBUGSTREAM, 'Incoming connection to %s' % repr(self._local)
+
self.push('220 %s ESMTP tmda-ofmipd' % (self.__fqdn))
self.set_terminator('\r\n')
@@ -643,11 +694,10 @@
return 501
self.__auth_username = username.lower()
self.__auth_password = password
- localip = self.__conn.getsockname()[0]
- os.environ['TCPLOCALIP'] = localip
+ os.environ['TCPLOCALIP'] = self._localip
if remoteauth['enable']:
# Try first with the remote auth
- if run_remoteauth(username, password, localip):
+ if run_remoteauth(username, password, self._localip):
return 1
if authprog:
# Then with the authprog
@@ -674,11 +724,10 @@
return 0
self.__auth_username = username.lower()
self.__auth_password = password
- localip = self.__conn.getsockname()[0]
- os.environ['TCPLOCALIP'] = localip
+ os.environ['TCPLOCALIP'] = self._localip
if remoteauth['enable']:
# Try first with the remote auth
- if run_remoteauth(username, password, localip):
+ if run_remoteauth(username, password, self._localip):
return 1
if authprog:
# Then with the authprog
@@ -984,36 +1033,9 @@
self.auth_challenge()
-class SMTPServer(asyncore.dispatcher):
- """The base class for the backend. Raises NotImplementedError if
- you try to use it."""
- 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
- self.set_reuse_addr()
- self.bind(localaddr)
- self.listen(5)
- print >> DEBUGSTREAM, \
- 'tmda-ofmipd started at %s\n\tListening on %s' % \
- (Util.unixdate(), proxyport)
-
- def readable(self):
- if len(asyncore.socket_map) > int(connections):
- # too many simultaneous connections
- return 0
- else:
- return 1
-
- def handle_accept(self):
- conn, addr = self.accept()
- print >> DEBUGSTREAM, 'Incoming connection from %s' % repr(addr)
- locaddr = conn.getsockname()
- self._localip = locaddr[0]
- print >> DEBUGSTREAM, 'Incoming connection to %s' % repr(locaddr)
- channel = SMTPChannel(self, conn, addr)
+class MessageProcessor:
+ """Base 'pure' SMTP message processing class.
+ Raises NotImplementedError if you try to use it."""
# API for "doing something useful with the message"
def process_message(self, peer, mailfrom, rcpttos, data):
@@ -1041,8 +1063,9 @@
raise NotImplementedError
-class DebuggingServer(SMTPServer):
+class DebuggingMessageProcessor(MessageProcessor):
"""Simply prints each message it receives on stdout."""
+
# Do something with the gathered message
def process_message(self, peer, mailfrom, rcpttos, data):
inheaders = 1
@@ -1057,9 +1080,47 @@
print '------------ END MESSAGE ------------'
-class PureProxy(SMTPServer):
- """Proxies all messages to a real smtpd which does final
- delivery."""
+class SMTPServer(asyncore.dispatcher, MessageProcessor):
+ """Run an SMTP server daemon - accept new socket connections and
+ process SMTP sessions on each conneciton."""
+
+ 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
+ self.set_reuse_addr()
+ self.bind(localaddr)
+ self.listen(5)
+ print >> DEBUGSTREAM, \
+ 'tmda-ofmipd started at %s\n\tListening on %s' % \
+ (Util.unixdate(), proxyport)
+
+ def readable(self):
+ if len(asyncore.socket_map) > int(connections):
+ # too many simultaneous connections
+ return 0
+ else:
+ return 1
+
+ def handle_accept(self):
+ conn = self.accept()[0]
+ self._channel = SMTPChannel(self, conn)
+
+
+class SMTPProcessor(asyncore.dispatcher, MessageProcessor):
+ """Run a single SMTP session."""
+
+ def __init__(self):
+ asyncore.dispatcher.__init__(self)
+ conn = socket.fromfd(0, socket.AF_INET, socket.SOCK_STREAM)
+ self._channel = SMTPChannel(self, conn)
+
+
+class PureProxy:
+ """Proxies all messages to a real smtpd which does final delivery."""
+
def process_message(self, peer, mailfrom, rcpttos, data):
lines = data.split('\n')
# Look for the last header
@@ -1104,10 +1165,11 @@
(VPopMail or VMailMgr) environment. It needs to behave differently from
the standard TMDA proxy in that authenticated users are not system
(/etc/passwd) users."""
+
def process_message(self, peer, mailfrom, rcpttos, data, auth_username):
# Set the TCPLOCALIP environment variable to support VPopMail's reverse
# IP domain mapping.
- os.environ['TCPLOCALIP'] = self._localip
+ os.environ['TCPLOCALIP'] = self._channel._localip
# Set up partial tmda-inject command line.
execdir = os.path.dirname(os.path.abspath(program))
inject_cmd = [os.path.join(execdir, 'tmda-inject')] + rcpttos
@@ -1132,7 +1194,11 @@
else:
# The 'prepend' is the system user in charge of this virtual
# domain.
- prepend = Util.getvdomainprepend(auth_username, vdomainspath)
+ prepend = Util.getvdomainuser(
+ auth_username,
+ vdomainspath,
+ vusersassignpath
+ )
if not prepend:
err = 'Error: "%s" is not a virtual domain' % (domain,)
print >> DEBUGSTREAM, err
@@ -1160,9 +1226,18 @@
os._exit(0)
+class VDomainProxyServer(VDomainProxy, SMTPServer):
+ pass
+
+
+class VDomainProxyProcessor(VDomainProxy, SMTPProcessor):
+ pass
+
+
class TMDAProxy(PureProxy):
"""Using this server for outgoing smtpd, the authenticated user
will have his mail tagged using his TMDA config file."""
+
def process_message(self, peer, mailfrom, rcpttos, data, auth_username):
if configdir is None:
# ~user/.tmda/
@@ -1202,6 +1277,14 @@
Util.pipecmd(inject_cmd, data)
+class TMDAProxyServer(TMDAProxy, SMTPServer):
+ pass
+
+
+class TMDAProxyProcessor(TMDAProxy, SMTPProcessor):
+ pass
+
+
def main():
# check permissions of authfile if using only remote
# authentication.
@@ -1213,11 +1296,17 @@
# try binding to the specified host:port
host, port = proxyport.split(':', 1)
if vhomescript:
- proxy = VDomainProxy((host, int(port)),
- ('localhost', 25))
+ if one_session:
+ proxy = VDomainProxyProcessor()
+ else:
+ proxy = VDomainProxyServer((host, int(port)),
+ ('localhost', 25))
else:
- proxy = TMDAProxy((host, int(port)),
- ('localhost', 25))
+ if one_session:
+ proxy = TMDAProxyProcessor()
+ else:
+ proxy = TMDAProxyServer((host, int(port)),
+ ('localhost', 25))
if running_as_root:
pw_uid = Util.getuid(username)
# check ownership of authfile if using only remote
@@ -1240,7 +1329,7 @@
# print "The default (background) behavior",
# print "could be changed in a future version."
# Try to fork to go to daemon unless foreground mode
- if not foreground:
+ if not (foreground or one_session):
signal.signal(signal.SIGHUP, signal.SIG_IGN) # ignore SIGHUP
if os.fork() != 0:
sys.exit()
_________________________________________________ tmda-workers mailing list ([EMAIL PROTECTED]) http://tmda.net/lists/listinfo/tmda-workers
