Skip to content

barryp/py-exim-localscan

Repository files navigation

Python Local Scan for Exim 4.x
===========================

    2003-07-05   Barry Pederson <bp@barryp.org>

This software embeds a Python interpreter into Exim 4.x, for running a 
Python-based local_scan function against incoming messages.

---------
COMPILING
---------
First, make sure you can build and run a plain Exim installation before 
attempting to add Python support.  Start by reading the toplevel Exim 
README file.  

Embedding Perl into Exim may cause linking conflicts with Python, you've 
been warned.

Once you've successfully built Exim, you may try patching the Exim 
Local/Makefile by running the patch_exim_makefile.py script included in 
this distribution.  The script takes one argument, the path to your
toplevel Exim build directory (that contains the "Local" directory the
Exim install docs told you to create).  The script will patch the Local/Makefile
and symlink the C sourcefile for the local_scan function (which should live
in the same directory as the patch script).

Rebuild Exim using the patched Makefile, if you don't see any errors then
you should be in business.  Install the new binary the same way as you did
with plain Exim.

----------------
CONFIGURING EXIM
----------------

There are a few options for this software that you may set in the
Exim 'configure' file, in the 'local_scan' section. 

    expy_enabled
   
       Type: boolean
       Default: true

       Gives you a way to cause this software to not try and execute
       any Python code, if you needed to disable it for some reason.
       For example:

           expy_enabled = false

    expy_exim_module
   
       Type: string
       Default: exim

       Specifies the name of the Python module that this software creates 
       to hold the builtin Exim functions, constants, and variables 
       described below.

    expy_path_add

       Type: string
       Default: unset

       Append one directory name to the Python sys.path list,
       useful for specifying where your local_scan module resides.  
       For example:

           expy_path_add = /foo/bar/mystuff

       If this variable isn't set, you'll have to place your module
       somewhere in the default Python path, such as the 
       site-packages directory.  

    expy_scan_module
   
       Type: string
       Default: exim_local_scan

       Name of the Python module you're supplying that contains
       a local_scan function.

    expy_scan_function
   
       Type: string
       Default: local_scan

       Name of the function within your module that this
       software will try and execute to perform the local_scan.

    expy_scan_failure

       Type: string
       Default: defer

       Return code in case the local_scan functions fails. Possible values:
       "accept", "defer", "deny".

expy_path_add is probably the only one you'll really need. The others
are handy if you don't care for their default values.


----------
RUNNING
----------

For every message that comes in, under the default Exim local_scan 
configuration settings described above, Exim will now call on embedded 
Python to import a module named 'exim_local_scan', and run a function 
in that module named 'local_scan'.  

Here is a sample "hello world" local scan module for Python:

------------
import exim

def local_scan():
    exim.log('Hello from Python')
    return exim.LOCAL_SCAN_ACCEPT

------------

If this works, you'll find a "Hello from Python" line added to each 
message entry in your Exim mainlog.


------------------------------------
WRITING A PYTHON LOCAL_SCAN FUNCTION
------------------------------------

Your "local_scan" function is called without any arguments, and should
return one of the LOCAL_SCAN_* constants (see below).  If you want to specify
some return_text, you may do so by returning a tuple containing the 
LOCAL_SCAN_* constant, along with a string.  For example:

    def local_scan():
        return exim.LOCAL_SCAN_TEMPREJECT, 'Come back later'

Several Exim functions, constants, and variables are available through
a module named 'exim' (that name is set by the expy_exim_module setting
described above), which user-supplied modules will want to import
(as in the "hello world" example above).  (Most of these descriptions
are basically copied from the Exim Local Scan documentation chapter 38, but
altered for Python)

    Functions
    ----------

        add_header(string):

            Adds a header to the message being scanned.  A newline
            is automatically added if necessary. Example:

                exim.add_header('X-Python: scanned')

            Please note that a header that's been folded into multiple
            lines of text must be added with a single function call. 

            For example, instead of:
 
                exim.add_header('x-foo: bar')
                exim.add_header(' baz')

            you'd need to use:

                exim.add_header('x-foo: bar\n baz')


        debug_print(string):
        
            If this function is called when Exim is not in debugging mode, it 
            does nothing. In debugging mode, it adds to the debugging output. 
            If the "pid" and/or "timestamp" debugging selectors are set, the pid 
            and/or timestamp are automatically added to each debug output line.
                        
            Newlines are NOT added automatically.
            
                exim.debug_print('some debug message\n')  
                    

        expand(string):

            Perform an Exim string-expansion.  For example:

                spooldir = exim.expand('$spool_directory')

            If the expansion fails, a ValueError exception is raised and
            the exception's error message includes the string Exim returns
            in C through expand_string_message.
        
        
        log(string [, which=LOG_MAIN]):

            Add a string to the specified log (defaults to LOG_MAIN). 
            For example:

                exim.log('Scanned by Python')
                exim.log('Rejected by Python', exim.LOG_REJECT)
                exim.log("We're freaking out here!', exim.LOG_PANIC)

        child_open(argv, envp, umask[, make_leader=False]):

           Create a child process that runs the command specified.
           "argv" and "envp" must be tuples of strings.

           The return value is (stdout, stdin, pid), where stdout and stdin are
           file descriptors to the appropriate pipes (stderr is joined with
           stdout). The environment may be specified, and a new umask supplied.

        child_open_exim(message[, sender="", sender_authentication=None])

           Submit a new message to Exim, returning the PID of the subprocess.
           Essentially, this is running 'exim -t -oem -oi -f sender -oMas auth'
           (-oMas is omitted if no authentication is provided).

        child_close(pid[, timeout=0])

           Wait for a child process to terminate, or for a timeout (in seconds)
           to expire. A timeout of zero (the default) means wait as long as it
           takes. The return value is the process ending status.

    Constants
    ----------
    (Python doesn't really have constants, you can assign other values to 
     these names if you really want to confuse yourself)

        LOG_MAIN
        LOG_PANIC
        LOG_REJECTLOG
            Used as the second argument to the log() function to specify
            which Exim log you want to write to.  Example:

                exim.log('Python was here', exim.LOG_MAIN)

        LOCAL_SCAN_ACCEPT
        LOCAL_SCAN_ACCEPT_FREEZE
        LOCAL_SCAN_ACCEPT_QUEUE
        LOCAL_SCAN_REJECT
        LOCAL_SCAN_REJECT_NOLOGHDR
        LOCAL_SCAN_TEMPREJECT
        LOCAL_SCAN_TEMPREJECT_NOLOGHDR
            Used one of these for the return value of your local scan function
         
        D_v
        D_local_scan
            Bitmasks for checking the debug_selector variable (see below)

        MESSAGE_ID_LENGTH
            The length of message identification strings. This is the id used internally
            by exim. The external version for use in Received: strings has a leading 'E'
            added to ensure it starts with a letter. 

        SPOOL_DATA_START_OFFSET
            The offset to the start of the data in the data file - this allows for
            the name of the data file to be present in the first line. 

        

    Variables
    ----------        

        debug_selector      (an integer)
        
            zero when no debugging is taking place. Otherwise, it is a bitmap
            of debugging selectors. Two bits are identified for use in local_scan()
            and defined as constants (see above):
                        
            The D_v bit is set when -v was present on the command line. This is a
            testing option that is not privileged - any caller may set it. All the
            other selector bits can be set only by admin users.
                                                
            The D_local_scan bit is provided for use by local_scan(); it is set by
            the +local_scan debug selector. It is not included in the default set
            of debugging bits.
                                                                        

        fd                  (an integer)

            The file descriptor for the file that contains the body of the 
            message (the -D file). The descriptor is positioned at the first 
            character of the body itself, having skipped over the beginning
            of the file which contains the message_id or the name of the data 
            file in the first line.
            
            The file is open for reading and writing, but updating it 
            is not recommended.

            Here is an example snippet of code that copies the body of the 
            message being scanned to a file named 'my_body':

                import os
                f = open('my_body', 'w')
                while 1:
                    s = os.read(exim.fd, 16384)
                    if not s:
                        break
                    f.write(s)   
        
        sender_address      (a string)

            The envelope sender address. For bounce messages this is 
            the empty string.

        headers

            A tuple of header_line objects.  Each header_line object has two 
            attributes: '.text', and '.type'.  

            The .text attribute is the entire text of the header line, which 
            may contain internal newlines, and should end in a newline.  It is
            not changable.

            The .type attribute is the one-character code Exim uses to identify 
            certain header lines (see chapter 48 of Exim manual).  It is changable, 
            but only to single-character values.  Normally you'd set it to '*' to 
            mark a header line as being deleted.

            Here's an example bit of code that deletes headers beginning with 'x-spam':

                for h in exim.headers:
                    if h.type != '*' and h.text.lower().startswith('x-spam'):
                        h.type = '*'

            Use the add_header() function (see above) to add new header lines, although
            you won't see them appear in this tuple.

        host_checking       (an integer)
        
            true during a -bh session

        interface_address   (a string)

            The IP address of the interface that received the message, as 
            a string.  This is None for locally submitted messages.

        interface_port      (an integer)

            The port on which this message was received.

        message_id          (a string)
        
            Message id of the message we're scanning
         
        received_protocol   (a string)

            The name of the protocol by which the message was received.

        recipients

            The list of accepted recipients.  You may modify or replace
            this list.  To blackhole a message you can use any of these 
            methods:

                exim.recipients = None
                exim.recipients = []
                del exim.recipients

            You could append a new address with:

                exim.recipients.append('postmaster@foobar.com')

            Or replace the list alltogether with:

                exim.recipients = ['quarantine@foobar.com', 'postmaster@foobar.com']

        sender_host_address         (a string)

            The IP address of the sending host, as a string. This is None for 
            locally-submitted messages.

        sender_host_authenticated   (a string)

            The name of the authentication mechanism that was used, or None if 
            the message was not received over an authenticated SMTP connection.

        sender_host_name            (a string)

            The name of the sending host, if known.

        sender_host_port            (an integer)

            The port on the sending host.        


Please note that the 'recipients' variable is the only one for which
modifications have any effect on Exim.  

------------------------
MORE ELABORATE EXAMPLES
------------------------

Here is a fancier local_scan function, that calls a 
separate 'scanner' module and catches any exceptions 
raised and logs them in the Exim rejectlog

-----------------------------------
import sys
import exim

# Wrap up the real scanning function in a tight
# try-except block, dump any raised python
# exceptions into the paniclog, and temporarily
# reject the message
#
def local_scan():
    try:
        import scanner
        return scanner.local_scan()
    except:
        pass

    #
    # Get the exception and traceback, format and convert
    # to individual lines, feed each line into exim log
    #
    import traceback
    einfo = sys.exc_info()
    lines = traceback.format_exception(einfo[0], einfo[1], einfo[2])
    lines = ''.join(lines).split('\n')
    for line in lines:
        exim.log(line, exim.LOG_PANIC)

    return exim.LOCAL_SCAN_TEMPREJECT
------------------------------------



Another useful bit of code for writing out a complete copy of a message
to a given filename (perhaps to feed into antivirus or spam-recognition 
software).

-----------
def copy_message(msg_filename):
    f = open(msg_filename, 'w')

    for h in exim.headers:
        if h.type != '*':
            f.write(h.text)

    f.write('\n')

    while 1:
        s = os.read(exim.fd, 16384)
        if not s:
            break
        f.write(s)

    f.close()
--------------------

### EOF ###

About

Embeds a Python interpreter into Exim 4.x. to allow Exim local_scan() functions to be written in Python

Resources

License

Stars

Watchers

Forks

Packages

No packages published