Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.Sign up
Embeds a Python interpreter into Exim 4.x. to allow Exim local_scan() functions to be written in Python
Fetching latest commit…
Cannot retrieve the latest commit at this time.
|Failed to load latest commit information.|
Python Local Scan for Exim 4.x =========================== 2003-07-05 Barry Pederson <firstname.lastname@example.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_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) 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('email@example.com') Or replace the list alltogether with: exim.recipients = ['firstname.lastname@example.org', 'email@example.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, einfo, einfo) 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 ###