postfix_smtp_verify_script

Postfix SMTP verify Script

(obsolete)

#!/usr/bin/env python
# 
#
# Postfix SMTP Access Policy Delegation sctip based on:
#
# https://github.com/syrusakbary/validate_email
# https://github.com/leoboiko/quotapolicy
#
# (c) 2016 Matthias Henze matthias@mhcsoftware.de
# This code is made available to you under the GNU GPL v3.
#
# To hand over SMTP VERIFY to an other MTA by SMTP
# 
# Do: 
#
# mkdir -p /var/spool/postfix/var/run
#
# Run the script as root, it will daemonize:
#
# smtpverify -i IP_OF_TARGET_SERVER
#
# Add this to "smtpd_recipient_restrictions" in Postfix "main.cf" as first entry (!):
#
#     check_policy_service unix:/var/run/smptverify.socket 
# 
# It should look like:
#
# smtpd_recipient_restrictions =
#     check_policy_service unix:/var/run/smptverify.socket
#     permit_mynetworks reject_unauth_destination
#     ${djigzo_rbl_clients}
#     ${djigzo_reject_unverified_recipient? reject_unverified_recipient}
#
# Do:
#
# postfix reload
#
# Now Ciphermail will ony accept Mail, accepted by the Mailserver.
#
# Reference: http://www.postfix.org/SMTPD_POLICY_README.html
#
#
 
 
import daemon
import SocketServer
import sys
import pwd
import subprocess
import os
import signal
import lockfile
import argparse
import atexit
 
 
import re
import smtplib
import logging
import socket
 
# All we are really doing is comparing the input string to one
# gigantic regular expression.  But building that regexp, and
# ensuring its correctness, is made much easier by assembling it
# from the "tokens" defined by the RFC.  Each of these tokens is
# tested in the accompanying unit test file.
#
# The section of RFC 2822 from which each pattern component is
# derived is given in an accompanying comment.
#
# (To make things simple, every string below is given as 'raw',
# even when it's not strictly necessary.  This way we don't forget
# when it is necessary.)
#
WSP = r'[\s]'                                        # see 2.2.2. Structured Header Field Bodies
CRLF = r'(?:\r\n)'                                   # see 2.2.3. Long Header Fields
NO_WS_CTL = r'\x01-\x08\x0b\x0c\x0f-\x1f\x7f'        # see 3.2.1. Primitive Tokens
QUOTED_PAIR = r'(?:\\.)'                             # see 3.2.2. Quoted characters
FWS = r'(?:(?:' + WSP + r'*' + CRLF + r')?' + \
      WSP + r'+)'                                    # see 3.2.3. Folding white space and comments
CTEXT = r'[' + NO_WS_CTL + \
        r'\x21-\x27\x2a-\x5b\x5d-\x7e]'              # see 3.2.3
CCONTENT = r'(?:' + CTEXT + r'|' + \
           QUOTED_PAIR + r')'                        # see 3.2.3 (NB: The RFC includes COMMENT here
# as well, but that would be circular.)
COMMENT = r'\((?:' + FWS + r'?' + CCONTENT + \
          r')*' + FWS + r'?\)'                       # see 3.2.3
CFWS = r'(?:' + FWS + r'?' + COMMENT + ')*(?:' + \
       FWS + '?' + COMMENT + '|' + FWS + ')'         # see 3.2.3
ATEXT = r'[\w!#$%&\'\*\+\-/=\?\^`\{\|\}~]'           # see 3.2.4. Atom
ATOM = CFWS + r'?' + ATEXT + r'+' + CFWS + r'?'      # see 3.2.4
DOT_ATOM_TEXT = ATEXT + r'+(?:\.' + ATEXT + r'+)*'   # see 3.2.4
DOT_ATOM = CFWS + r'?' + DOT_ATOM_TEXT + CFWS + r'?' # see 3.2.4
QTEXT = r'[' + NO_WS_CTL + \
        r'\x21\x23-\x5b\x5d-\x7e]'                   # see 3.2.5. Quoted strings
QCONTENT = r'(?:' + QTEXT + r'|' + \
           QUOTED_PAIR + r')'                        # see 3.2.5
QUOTED_STRING = CFWS + r'?' + r'"(?:' + FWS + \
                r'?' + QCONTENT + r')*' + FWS + \
                r'?' + r'"' + CFWS + r'?'
LOCAL_PART = r'(?:' + DOT_ATOM + r'|' + \
             QUOTED_STRING + r')'                    # see 3.4.1. Addr-spec specification
DTEXT = r'[' + NO_WS_CTL + r'\x21-\x5a\x5e-\x7e]'    # see 3.4.1
DCONTENT = r'(?:' + DTEXT + r'|' + \
           QUOTED_PAIR + r')'                        # see 3.4.1
DOMAIN_LITERAL = CFWS + r'?' + r'\[' + \
                 r'(?:' + FWS + r'?' + DCONTENT + \
                 r')*' + FWS + r'?\]' + CFWS + r'?'  # see 3.4.1
DOMAIN = r'(?:' + DOT_ATOM + r'|' + \
         DOMAIN_LITERAL + r')'                       # see 3.4.1
ADDR_SPEC = LOCAL_PART + r'@' + DOMAIN               # see 3.4.1
 
# A valid address will match exactly the 3.4.1 addr-spec.
VALID_ADDRESS_REGEXP = '^' + ADDR_SPEC + '$'
 
 
class DummyConfig:
    pass
 
# filled by argparse below
c = DummyConfig()
devnull = open(os.devnull, 'w')
 
def validate_email(email, ip, smtp_timeout=10):
    """Indicate whether the given string is a valid email address
    according to the 'addr-spec' portion of RFC 2822 (see section
    3.4.1).  Parts of the spec that are marked obsolete are *not*
    included in this test, and certain arcane constructions that
    depend on circular definitions in the spec may not pass, but in
    general this should correctly identify any email address likely
    to be in use as of 2011."""
 
    try:
        assert re.match(VALID_ADDRESS_REGEXP, email) is not None
        try:
            smtp = smtplib.SMTP(timeout=smtp_timeout)
            smtp.connect(ip)
            status, _ = smtp.helo()
            if status != 250:
                smtp.quit()
                if c.debug:
                    logger.debug(u'%s answer: %s - %s', ip, status, _)
                return None
            smtp.mail('')
            status, _ = smtp.rcpt(email)
            if status == 250:
                smtp.quit()
                return True
            if c.debug:
                logger.debug(u'%s answer: %s - %s', ip, status, _)
            smtp.quit()
        except smtplib.SMTPServerDisconnected:  # Server not permits verify user
            if c.debug:
                logger.debug(u'%s disconected.', ip)
        except smtplib.SMTPConnectError:
            if c.debug:
                logger.debug(u'Unable to connect to %s.', ip)
        return None
    except AssertionError:
        return False
    except (socket.error) as e:
        if c.debug:
            logger.debug('ServerError or socket.error exception raised (%s).', e)
        return None
    return True
 
class SmtpSocketHandler(SocketServer.StreamRequestHandler):
 
    def handle(self):
        recipient = None
        action = 'OK'
 
        while True:
            line = self.rfile.readline().strip()
            if line == '':
                break
 
            a = line.split('=')
            key = a[0]
            val = '='.join(a[1:])
 
            if key == 'recipient':
                recipient = val
                break
 
        if recipient:
            ok = validate_email(recipient, c.ip)
            if not ok:
                action = 'REJECT ' \
                        + recipient \
                        + ' user unknowen' \
                        + ' (' + str(ok) + ')'
 
        if c.debug:
            logger.debug( u'%s: sending %s', recipient, action)
        self.wfile.write('action=' + action + "\n")
        self.wfile.write("\n")
 
class ForkingUnixStreamServer(
        SocketServer.UnixStreamServer,
        SocketServer.ForkingMixIn
        ):
    pass
 
def cleanup(signum=None, stack=None):
    if os.path.exists(c.sockpath):
        os.unlink(c.sockpath)
    if os.path.exists(c.pidpath):
        os.unlink(c.pidpath)
 
 
# parse arguments
p = argparse.ArgumentParser()
 
p.add_argument('-i', '--ip', required=True,
        dest='ip',
        help='(Required) SMTP server IP',
        )
 
p.add_argument('-p', '--pidfile', default='/var/spool/postfix/var/run/smptverify.pid',
        dest='pidpath',
        help='Pidfile, relative to HOMEDIR (default: %(default)s )')
 
p.add_argument('-s', '--socketfile', default='/var/spool/postfix/var/run/smptverify.socket',
        dest='sockpath',
        help='Socket file, relative to HOMEDIR (default: %(default)s )')
 
p.add_argument('-d', '--recipient-delimiter', default='+',
        help='Set this to Postfix recipient_delimiter parameter')
 
p.add_argument('--debug', action='store_true',
        help="Don't fork; print messages to stdout")
 
c.__dict__.update(p.parse_args().__dict__)
 
 
# check things
 
atexit.register(cleanup)
if os.path.exists(c.pidpath):
    if c.pidpath[0] == '/':
        abs_pidpath = c.pidpath
 
    sys.stderr.write("daemon seems to be running: %s exists\n"
            % abs_pidpath)
    sys.exit(1)
 
# setup socket
if os.path.exists(c.sockpath): os.unlink(c.sockpath)
server = ForkingUnixStreamServer(c.sockpath, SmtpSocketHandler)
os.chmod(c.sockpath, 0666)
 
 
# setup daemonization
context = daemon.DaemonContext()
context.files_preserve=[server.fileno(), devnull]
context.pidfile = lockfile.FileLock(c.pidpath)
 
if c.debug:
    context.detach_process = False
    context.stdin = sys.stdin
    context.stdout = sys.stdout
    context.stderr = sys.stderr
    logger = logging.getLogger('smtpverify')
    ch = logging.StreamHandler()
    ch.setLevel(logging.ERROR)
    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
    ch.setFormatter(formatter)
    logger.addHandler(ch)
    logger.setLevel(logging.DEBUG)
else:
    logger = None
    context.detach_process = True
 
with context:
    try:
        with open(c.pidpath, 'w') as p:
            # for start-stop-daemon etc.
            p.write(str(os.getpid()))
 
        server.serve_forever()
    finally:
        cleanup()

  • postfix_smtp_verify_script.txt
  • Zuletzt geändert: 2017/03/17 12:37
  • (Externe Bearbeitung)