At present, tmda-ofmipd's support of qmail virtual domains is limited to the minimum required for the two popular vdomain packages - vpopmail (which always uses the same user ID for all users) and vmailmgr (which uses a separate user ID for each virtual domain)

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

Reply via email to