[Exim] Python embedded in Exim

Etusivu
Poista viesti
Vastaa
Lähettäjä: Jeffrey C. Ollie
Päiväys:  
Vastaanottaja: exim-users
Kopio: mailman-developers, python-dev
Aihe: [Exim] Python embedded in Exim
Well, I had an epiphany over the US Thanksgiving holiday weekend (or
maybe it was a brain aneurism) so I've spent some late nights and some
time stolen from work and come up with a first pass at embedding
Python in Exim.

I imagine that embedding Python in Exim will be interesting to those
folks writing virus scanners or for VERY tight integration of Mailman
with Exim. You can also do all of those things that you can do with
the Perl string expansions.

Note: this code is VERY lightly tested. It compiles on my home
hacked-up RedHat Linux system and works on a couple of simple tests
that I've done, but no real e-mails have passed through this code yet.
I'm tossing this out for public comment on the design and
implementation... It's been a while since I've done a lot of C
coding.

Unfortunately, you must embed Python 2.0. This is because previous
versions of Python used a hacked copy of PCRE as it's regular
expression engine that conflicts with the copy of PCRE in Exim.

Starting in Python 2.0 the default regular expression is a completely
new one called SRE that supports Unicode. The PCRE-based regular
expression engine is still included in Python 2.0 but I get around
that by creating a private copy of the Python library and delete the
offending object modules. You could do that in versions of Python
prior to 2.0 but a lot of the standard library depends on regular
expressions I didn't think that there was much point in trying to make
the embedding work with 1.5.2 or 1.6.

Well, on to the patch itself. The patch is made against Exim v3.20.
After you've patched the source code, you need to set four variables
in the Local/Makefile:

EXIM_PYTHON=python.o
PYTHON_INC=-I/usr/local/include/python2.0
PYTHON_LIB=/usr/local/lib/python2.0/config/libpython2.0.a
PYTHON_EXTRA_LIBS=-lpthread -ldl -lutil

Then build Exim as usual.

There are three runtime directives that control the embedding, all of which
are optional:

python_at_start        boolean - force startup of Python interpreter
python_module_paths    colon separated list of paths to append to sys.path
python_initial_import   colon separated list of modules to import at
                        interpreter initialization time


There are also two new command line switches -ys and -yd that will
force an immediate startup of Python or delay startup, overriding
python_at_start.

Then you use it:

${python{<module>.<function>}{<arg1>}...}

You must specify the module name and the function name. The named
module will be imported automatically. And currently you must specify
a function that returns a string (I'll look into adding accessing
attributes, methods of classes, and converting non-strings into
strings). There can be up to eight arguments. Each argument will be
expanded by Exim before it's passed to the Python interpreter.

Your python code has access to a module called "exim", which currently
defines two things:

exim.expand_string(s) which calls back to Exim to expand a string and
exim.error            which is an exception object used by expand_string


One simple example:

${python{string.upper}{$local_part}}

Error reporting isn't the best right now, I'm going to look into
formatting exceptions and tracebacks for more meaningful error
messages.

Well, I think that's the gist of it. I'll work on patches to the
documentation when this thing becomes more stable. As I said before
I've only lightly tested this... beware!

Comments, criticisms, suggestions, flames welcome...

Jeff

Index: exim/OS/Makefile-Base
diff -u exim/OS/Makefile-Base:1.1.1.1 exim/OS/Makefile-Base:1.1.1.1.2.3
--- exim/OS/Makefile-Base:1.1.1.1    Mon Nov 27 08:18:15 2000
+++ exim/OS/Makefile-Base    Tue Nov 28 16:49:25 2000
@@ -192,6 +192,16 @@
     @chmod a+x ../util/convert4r3
     @echo ">>> convert4r3 script built in util directory"; echo ""


+libpython.a: $(PYTHON_LIB)
+    if [ -n $(PYTHON_LIB) ] ;\
+    then \
+        cp $(PYTHON_LIB) libpython.a ;\
+        ar d libpython.a pcremodule.o pypcre.o ;\
+        ranlib libpython.a ;\
+    else \
+        ar cq libpython.a ;\
+        ranlib libpython.a ;\
+    fi


 # Targets for final binaries; the main one has a build number which is
 # updated each time. We don't bother with that for the auxiliaries.
@@ -201,11 +211,12 @@
         header.o host.o log.o match.o moan.o os.o parse.o queue.o \
         readconf.o retry.o rewrite.o \
         route.o search.o smtp_in.o smtp_out.o spool_in.o spool_out.o \
-        store.o string.o tls.o tod.o transport.o tree.o verify.o $(EXIM_PERL)
+        store.o string.o tls.o tod.o transport.o tree.o verify.o $(EXIM_PERL) \
+    $(EXIM_PYTHON)


 exim:   libident/libident.a pcre/libpcre.a lookups/lookups.a auths/auths.a \
         directors/directors.a routers/routers.a transports/transports.a \
-        $(OBJ_EXIM) version.c
+        $(OBJ_EXIM) version.c libpython.a
     awk '{ print ($$1+1) }' cnumber.h > cnumber.temp
     /bin/rm -f cnumber.h; mv cnumber.temp cnumber.h
     $(CC) -c $(CFLAGS) $(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE) version.c
@@ -215,7 +226,8 @@
       routers/routers.a transports/transports.a lookups/lookups.a \
       auths/auths.a \
       $(LIBS) $(LIBS_EXIM) $(IPV6_LIBS) $(EXTRALIBS) $(EXTRALIBS_EXIM) \
-      $(DBMLIB) $(LIBRESOLV) $(LOOKUP_LIBS) $(PERL_LIBS) $(TLS_LIBS)
+      $(DBMLIB) $(LIBRESOLV) $(LOOKUP_LIBS) $(PERL_LIBS) libpython.a $(PYTHON_EXTRA_LIBS) \
+      $(TLS_LIBS)
     $(EXIM_CHMOD)
     @echo " "
     @echo ">>> exim binary built"
@@ -316,6 +328,11 @@


 perl.o:          $(HDRS) perl.c
     $(PERL_CC) $(PERL_CCOPTS) $(CFLAGS) $(INCLUDE) -c perl.c
+
+# Build instructions for python.o for when EXIM_PYTHON is set
+
+python.o:          $(HDRS) python.c
+    $(CC) -c $(CFLAGS) $(INCLUDE) $(PYTHON_INC) -I. python.c


# Dependencies for the "ordinary" exim modules

Index: exim/scripts/MakeLinks
diff -u exim/scripts/MakeLinks:1.1.1.1 exim/scripts/MakeLinks:1.1.1.1.2.1
--- exim/scripts/MakeLinks:1.1.1.1    Mon Nov 27 08:18:16 2000
+++ exim/scripts/MakeLinks    Mon Nov 27 08:27:46 2000
@@ -189,6 +189,7 @@
 ln -s ../src/moan.c            moan.c
 ln -s ../src/parse.c           parse.c
 ln -s ../src/perl.c            perl.c
+ln -s ../src/python.c          python.c
 ln -s ../src/queue.c           queue.c
 ln -s ../src/readconf.c        readconf.c
 ln -s ../src/retry.c           retry.c
Index: exim/src/EDITME
diff -u exim/src/EDITME:1.1.1.2 exim/src/EDITME:1.1.1.2.2.4
--- exim/src/EDITME:1.1.1.2    Mon Nov 27 08:18:54 2000
+++ exim/src/EDITME    Tue Nov 28 16:20:19 2000
@@ -260,6 +260,10 @@
 # PERL_CCOPTS=
 # PERL_LIBS=


+# EXIM_PYTHON=python.o
+# PYTHON_INC=-I/usr/local/include/python2.0
+# PYTHON_LIB=/usr/local/lib/python2.0/config/libpython2.0.a
+# PYTHON_EXTRA_LIBS=-lpthread -ldl -lutil

 # This parameter sets the maximum length of the header portion of a message
 # that Exim is prepared to process. The default setting is one megabyte. There
Index: exim/src/config.h.defaults
diff -u exim/src/config.h.defaults:1.1.1.1 exim/src/config.h.defaults:1.1.1.1.2.1
--- exim/src/config.h.defaults:1.1.1.1    Mon Nov 27 08:18:16 2000
+++ exim/src/config.h.defaults    Mon Nov 27 08:43:04 2000
@@ -36,6 +36,8 @@


#define EXIM_PERL

+#define EXIM_PYTHON
+
#define HAVE_SA_LEN
#define HEADER_MAXSIZE (1024*1024)

Index: exim/src/exim.c
diff -u exim/src/exim.c:1.1.1.2 exim/src/exim.c:1.1.1.2.2.4
--- exim/src/exim.c:1.1.1.2    Mon Nov 27 08:18:54 2000
+++ exim/src/exim.c    Tue Nov 28 00:46:52 2000
@@ -351,6 +351,9 @@
 #ifdef EXIM_PERL
 int  perl_start_option = 0;
 #endif
+#ifdef EXIM_PYTHON
+int  python_start_option = 0;
+#endif
 int  recipients_arg = argc;
 int  sender_address_domain = 0;
 int  test_retry_arg = -1;
@@ -583,6 +586,17 @@
 simply recorded for checking and handling afterwards. Do a high-level switch
 on the second character (the one after '-'), to save some effort. */


+#ifdef EXIM_PYTHON
+opt_python_original_argc = argc;
+opt_python_original_argv = store_get(sizeof(char *) * (argc + 1));
+opt_python_original_argv[argc] = NULL;
+
+for (i = 0; i < argc; i++)
+  {
+  opt_python_original_argv[i] = string_copy(argv[i]);
+  }
+#endif
+
 for (i = 1; i < argc; i++)
   {
   BOOL badarg = FALSE;
@@ -1521,6 +1535,17 @@
     break;
     #endif


+    /* -ys: force Python startup; -yd force delayed Python startup */
+
+    #ifdef EXIM_PYTHON
+    case 'y':
+    if (*argrest == 's' && argrest[1] == 0) python_start_option = 1;
+    else
+      if (*argrest == 'd' && argrest[1] == 0) python_start_option = -1;
+    else badarg = TRUE;
+    break;
+    #endif
+


     case 'q':


@@ -1967,6 +1992,27 @@
   opt_perl_started = TRUE;
   }
 #endif /* EXIM_PERL */
+
+/* Start up Python interpreter if Python support is configured and
+there is a and the configuration or the command line specifies
+initializing starting. Note that the global variables are actually
+called opt_python_xxx. */
+
+#ifdef EXIM_PYTHON
+if (python_start_option != 0)
+  opt_python_at_start = (python_start_option > 0);
+if (opt_python_at_start)
+  {
+  char *errstr;
+  DEBUG(9) debug_printf("Starting Python interpreter\n");
+  errstr = init_python();
+  if (errstr != NULL)
+    {
+    fprintf(stderr, "exim: error during Python startup: %s\n", errstr);
+    return EXIT_FAILURE;
+    }
+  }
+#endif /* EXIM_PYTHON */


 /* Log the arguments of the call if the configuration file said so. This is
 a debugging feature for finding out what arguments certain MUAs actually use.
Index: exim/src/expand.c
diff -u exim/src/expand.c:1.1.1.2 exim/src/expand.c:1.1.1.2.2.3
--- exim/src/expand.c:1.1.1.2    Mon Nov 27 08:18:54 2000
+++ exim/src/expand.c    Tue Nov 28 00:23:10 2000
@@ -1318,7 +1318,7 @@


 Operators:
    domain                  extracts domain from an address
-   escape                  escapes non-printing characters
+   escape                  escapes non-printing characters 
    expand                  expands the string one more time
    hash_<n>_<m>            hash the string, making one that is of length n,
                              using m (default 26) characters from hashcodes
@@ -1865,6 +1865,100 @@
     continue;
     }
   #endif /* EXIM_PERL */
+
+  /* If Perl support is configured, handle calling embedded perl subroutines,
+  unless locked out at this time. Syntax is ${perl{sub}} or ${perl{sub}{arg}}
+  or ${perl{sub}{arg1}{arg2}} or up to a maximum of EXIM_PERL_MAX_ARGS
+  arguments (defined below). */
+
+  #ifdef EXIM_PYTHON
+  #define EXIM_PYTHON_MAX_ARGS 8
+
+  if (strcmp(name, "python") == 0)
+    {
+    int i = 0;
+    char *sub_name;
+    char *sub_arg[EXIM_PYTHON_MAX_ARGS + 1];
+    char *new_yield;
+
+    if (expand_forbid_python)
+      {
+      expand_string_message = "Python calls are not permitted";
+      goto EXPAND_FAILED;
+      }
+
+    while (isspace((uschar)*s)) s++;
+    if (*s != '{') goto EXPAND_FAILED_CURLY;
+    sub_name = expand_string_internal(s+1, TRUE, &s, FALSE);
+    if (sub_name == NULL) goto EXPAND_FAILED;
+    while (isspace((uschar)*s)) s++;
+    if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+
+    while (isspace((uschar)*s)) s++;
+
+    while (*s == '{')
+      {
+      if (i == EXIM_PYTHON_MAX_ARGS)
+        {
+        expand_string_message =
+          string_sprintf("Too many arguments passed to Python subroutine \"%s\" "
+            "(max is %d)", sub_name, EXIM_PYTHON_MAX_ARGS);
+        goto EXPAND_FAILED;
+        }
+      sub_arg[i] = expand_string_internal(s+1, TRUE, &s, FALSE);
+      if (sub_arg[i++] == NULL) goto EXPAND_FAILED;
+      while (isspace((uschar)*s)) s++;
+      if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+      while (isspace((uschar)*s)) s++;
+      }
+
+    if (*s++ != '}') goto EXPAND_FAILED_CURLY;
+    sub_arg[i] = 0;
+
+    /* Start the interpreter if necessary */
+
+    if (!opt_python_started)
+      {
+      char *initerror;
+      DEBUG(9) debug_printf("Starting Python interpreter\n");
+      initerror = init_python();
+      if (initerror != NULL)
+        {
+        expand_string_message =
+          string_sprintf("error during Python startup: %s\n", initerror);
+        goto EXPAND_FAILED;
+        }
+      }
+
+    /* Call the function */
+
+    new_yield = call_python_cat(yield, &size, &ptr, &expand_string_message,
+      sub_name, sub_arg);
+
+    /* NULL yield indicates failure; if the message pointer has been set to
+    NULL, the yield was undef, indicating a forced failure. Otherwise the
+    message will indicate some kind of Perl error. */
+
+    if (new_yield == NULL)
+      {
+      if (expand_string_message == NULL)
+        {
+        expand_string_message =
+          string_sprintf("Python subroutine \"%s\" returned undef to force "
+            "failure", sub_name);
+        expand_string_forcedfail = TRUE;
+        }
+      goto EXPAND_FAILED;
+      }
+
+    /* Yield succeeded. Ensure forcedfail is unset, just in case it got
+    set during a callback from Python. */
+
+    expand_string_forcedfail = FALSE;
+    yield = new_yield;
+    continue;
+    }
+  #endif /* EXIM_PYTHON */


/* Handle character translation for "tr" */

Index: exim/src/functions.h
diff -u exim/src/functions.h:1.1.1.2 exim/src/functions.h:1.1.1.2.2.1
--- exim/src/functions.h:1.1.1.2    Mon Nov 27 08:18:54 2000
+++ exim/src/functions.h    Mon Nov 27 10:13:25 2000
@@ -16,6 +16,12 @@
 extern char *init_perl(char *);
 #endif


+#ifdef EXIM_PYTHON
+extern char *call_python_cat(char *, int *, int *, char **, char *, char **);
+extern void  cleanup_python(void);
+extern char *init_python(void);
+#endif
+
 #ifdef SUPPORT_TLS
 extern BOOL  tls_client_start(int, host_item *, address_item *, char *,
                char *, char *, char *, char *, int);
Index: exim/src/globals.c
diff -u exim/src/globals.c:1.1.1.1 exim/src/globals.c:1.1.1.1.2.2
--- exim/src/globals.c:1.1.1.1    Mon Nov 27 08:18:16 2000
+++ exim/src/globals.c    Mon Nov 27 11:10:15 2000
@@ -35,6 +35,15 @@
 BOOL   opt_perl_started       = FALSE;
 #endif


+#ifdef EXIM_PYTHON
+BOOL   opt_python_at_start       = FALSE;
+char  *opt_python_module_paths   = NULL;
+char  *opt_python_initial_import = NULL;
+BOOL   opt_python_started        = FALSE;
+int    opt_python_original_argc  = 0;
+char **opt_python_original_argv  = NULL;
+#endif
+
 #ifdef HAVE_AUTH
 BOOL   auth_always_advertise  = TRUE;
 char  *auth_hosts             = NULL;
@@ -310,6 +319,7 @@
 BOOL   expand_forbid_exists   = FALSE;
 BOOL   expand_forbid_lookup   = FALSE;
 BOOL   expand_forbid_perl     = FALSE;
+BOOL   expand_forbid_python   = FALSE;
 int    expand_nlength[EXPAND_MAXN+1];
 int    expand_nmax            = -1;
 char  *expand_nstring[EXPAND_MAXN+1];
Index: exim/src/globals.h
diff -u exim/src/globals.h:1.1.1.1 exim/src/globals.h:1.1.1.1.2.2
--- exim/src/globals.h:1.1.1.1    Mon Nov 27 08:18:16 2000
+++ exim/src/globals.h    Mon Nov 27 11:10:15 2000
@@ -21,6 +21,15 @@
 extern BOOL   opt_perl_started;       /* Set once interpreter started */
 #endif


+#ifdef EXIM_PYTHON
+extern BOOL   opt_python_at_start;       /* start Python at startup */
+extern char  *opt_python_module_paths;   /* list of paths to append to sys.path */
+extern char  *opt_python_initial_import; /* list of modules to import at interpreter initialization time */
+extern BOOL   opt_python_started;        /* set once Python interpreter started */
+extern int    opt_python_original_argc;
+extern char **opt_python_original_argv;
+#endif
+
 #ifdef HAVE_AUTH
 extern BOOL   auth_always_advertise;  /* If FALSE, advertise only when needed */
 extern char  *auth_hosts;             /* These must authenticate */
@@ -208,6 +217,7 @@
 extern BOOL   expand_forbid_exists;   /* Lock out exists checking */
 extern BOOL   expand_forbid_lookup;   /* Lock out lookups */
 extern BOOL   expand_forbid_perl;     /* Lock out Perl calls */
+extern BOOL   expand_forbid_python;   /* Lock out Python calls */
 extern int    expand_nlength[];       /* Lengths of numbered strings */
 extern int    expand_nmax;            /* Max numerical value */
 extern char  *expand_nstring[];       /* Numbered strings */
Index: exim/src/python.c
diff -u /dev/null exim/src/python.c:1.1.2.11
--- /dev/null    Tue Nov 28 16:50:15 2000
+++ exim/src/python.c    Tue Nov 28 16:22:03 2000
@@ -0,0 +1,291 @@
+#include "exim.h"
+
+#include "Python.h"
+
+static void init_exim_module(void);
+
+char *init_python(void)
+{
+   if (opt_python_started)
+   {
+      return NULL;
+   }
+
+   Py_SetProgramName(opt_python_original_argv[0]);
+   
+   Py_Initialize();
+   
+   PySys_SetArgv(opt_python_original_argc, opt_python_original_argv);
+
+   init_exim_module();
+
+   if (opt_python_module_paths)
+   {
+      char *listptr = opt_python_module_paths;
+      int separator = 0;
+      char buffer[256];
+      PyObject *path;
+      PyObject *sys_module;
+      PyObject *sys_dict;
+      PyObject *sys_path;
+      PyObject *name;
+
+      name = PyString_FromString("sys");
+      if (name == NULL)
+      {
+     PyErr_Clear();
+     return "problem creating string \"sys\"";
+      }
+
+      sys_module = PyImport_Import(name);
+      Py_DECREF(name);
+
+      if (sys_module == NULL)
+      {
+     PyErr_Clear();
+     return "problem trying to import sys";
+      }
+
+      sys_dict = PyModule_GetDict(sys_module);
+      if (sys_dict == NULL)
+      {
+     PyErr_Clear();
+     return "problem trying get dictionary from module sys";
+      }
+
+      sys_path = PyDict_GetItemString(sys_dict, "path");
+      if (sys_path == NULL)
+      {
+     PyErr_Clear();
+     return "problem trying to get sys.path";
+      }
+
+      while(string_nextinlist(&listptr, &separator, buffer, sizeof(buffer)))
+      {
+     path = PyString_FromString(buffer);
+     if (path == NULL)
+     {
+        PyErr_Clear();
+        return string_sprintf("problem while allocating Python string for \"%s\"", buffer);
+     }
+
+     if(!PyList_Append(sys_path, path))
+     {
+        PyErr_Clear();
+        return string_sprintf("problem while insering string \"%s\"into sys.path", buffer);
+     }
+      }
+   }
+
+   if (opt_python_initial_import)
+   {
+      char *listptr = opt_python_initial_import;
+      int separator = 0;
+      char buffer[256];
+      PyObject *module;
+      PyObject *name;
+
+      while(string_nextinlist(&listptr, &separator, buffer, sizeof(buffer)))
+      {
+     name = PyString_FromString(buffer);
+     if (name == NULL)
+     {
+        PyErr_Clear();
+        return string_sprintf("problem creating string \"%s\"", buffer);
+     }
+
+     module = PyImport_Import(name);
+     Py_DECREF(name);
+
+     if (module == NULL)
+     {
+        PyErr_Clear();
+        return string_sprintf("problem importing module %s", buffer);
+     }
+      }
+   }
+
+      
+   opt_python_started = TRUE;
+   return NULL;
+}
+
+void cleanup_python(void)
+{
+  Py_Finalize();
+}
+
+static PyObject *find_function(char *name, char **errstrp)
+{
+   PyObject *module_name;
+   PyObject *function_name;
+   PyObject *module;
+   PyObject *dictionary;
+   PyObject *result;
+
+   char *ptr;
+
+   ptr = strrchr(name, '.');
+   
+   if (ptr == NULL)
+   {
+      *errstrp = string_sprintf("function name must include module: <module>.<name>");
+      return NULL;
+   }
+
+   function_name = PyString_FromString(ptr + 1);
+
+   if (function_name == NULL)
+   {
+      PyErr_Clear();
+      *errstrp = string_sprintf("error trying to allocate function name");
+      return NULL;
+   }
+
+   module_name = PyString_FromStringAndSize(name, ptr - name);
+
+   if (function_name == NULL)
+   {
+      PyErr_Clear();
+      Py_DECREF(function_name);
+      *errstrp = string_sprintf("error trying to allocate module name");
+      return NULL;
+   }
+
+   module = PyImport_Import(module_name);
+
+   Py_DECREF(module_name);
+
+   if (module == NULL)
+   {
+      PyErr_Clear();
+      Py_DECREF(function_name);
+      *errstrp = string_sprintf("error trying to import module while trying to find %s", name);
+      return NULL;
+   }
+
+   dictionary = PyModule_GetDict(module);
+
+   result = PyDict_GetItem(dictionary, function_name);
+
+   Py_DECREF(function_name);
+
+   if (!PyCallable_Check(result))
+   {
+      PyErr_Clear();
+      *errstrp = string_sprintf("%s is not a callable object", name);
+      return NULL;
+   }
+
+   return result;
+}
+
+char *call_python_cat(char *yield, int *sizep, int *ptrp, char **errstrp,
+              char *name, char **arg)
+{
+   PyObject *function;
+   PyObject *arguments;
+   PyObject *result;
+   int index;
+   int count=0;
+   char **p;
+   char *buffer;
+   int length;
+
+   function = find_function(name, errstrp);
+
+   if (function == NULL)
+      return NULL;
+
+   p = arg;
+
+   while(*p)
+   {
+      count++;
+      p++;
+   }
+
+   arguments = PyTuple_New(count);
+
+   index = 0;
+   while(index < count)
+   {
+
+      PyTuple_SetItem(arguments, index, PyString_FromString(arg[index]));
+      index++;
+   }
+
+   result = PyObject_CallObject(function, arguments);
+
+   Py_DECREF(arguments);
+
+   if (result == NULL)
+   {
+      PyErr_Clear();
+      *errstrp = string_sprintf("error in Python function %s", name);
+      return NULL;
+   }
+
+   if(!PyString_Check(result))
+   {
+      Py_DECREF(result);
+      *errstrp = string_sprintf("Python function %s returned non-string", name);
+      return NULL;
+   }
+
+   if(PyString_AsStringAndSize(result, &buffer, &length))
+   {
+      PyErr_Clear();
+      Py_DECREF(result);
+      *errstrp = string_sprintf("could not extract the results of Python function %s", name);
+      return NULL;
+   }
+
+   yield = string_cat(yield, sizep, ptrp, buffer, length);
+
+   Py_DECREF(result);
+
+   return yield;
+}
+
+static PyObject *exim_error = NULL;
+
+static PyObject *exim_expand_string(PyObject *self, PyObject *args)
+{
+   char *i;
+   char *o;
+
+   if(!PyArg_ParseTuple(args, "s", i))
+      return NULL;
+   
+   o = expand_string(i);
+   
+   if (o == NULL)
+   {
+      PyErr_SetString(exim_error, expand_string_message);
+      return NULL;
+   }
+  
+   return PyString_FromString(o);
+}
+
+static PyMethodDef exim_methods[] = {
+   {"expand_string", exim_expand_string, 1},
+   {NULL,            NULL} /* sentinel */
+};
+
+static void init_exim_module(void)
+{
+   PyObject *m;
+   PyObject *d;
+   PyObject *o;
+
+   m = PyImport_AddModule("exim");
+   d = PyModule_GetDict(m);
+
+   Py_InitModule("exim", exim_methods);
+
+   exim_error = PyErr_NewException("exim.error", NULL, NULL);
+   PyDict_SetItemString(d, "error", exim_error);
+}
+
Index: exim/src/readconf.c
diff -u exim/src/readconf.c:1.1.1.1 exim/src/readconf.c:1.1.1.1.2.2
--- exim/src/readconf.c:1.1.1.1    Mon Nov 27 08:18:16 2000
+++ exim/src/readconf.c    Mon Nov 27 10:14:17 2000
@@ -160,6 +160,11 @@
   { "perl_at_start",            opt_bool,        &opt_perl_at_start },
   { "perl_startup",             opt_stringptr,   &opt_perl_startup },
 #endif
+#ifdef EXIM_PYTHON
+  { "python_at_start",          opt_bool,        &opt_python_at_start },
+  { "python_module_paths",      opt_stringptr,   &opt_python_module_paths },
+  { "python_initial_import",    opt_stringptr,   &opt_python_initial_import },
+#endif
 #ifdef LOOKUP_PGSQL
   { "pgsql_servers",            opt_stringptr,   &pgsql_servers },
 #endif