Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

https://github.com/barryp/py-exim-localscan

Embeds a Python interpreter into Exim 4.x. to allow Exim local_scan() functions to be written in Python
https://github.com/barryp/py-exim-localscan

Last synced: about 2 months ago
JSON representation

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

Lists

README

        

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

2003-07-05 Barry Pederson

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('[email protected]')

Or replace the list alltogether with:

exim.recipients = ['[email protected]', '[email protected]']

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 ###