elspy API Reference
===================

This is reference documentation on how to write an Exim local_scan()
function in Python using elspy; I have not yet written a tutorial for
elspy (but see the ``examples/`` directory of the elspy source
distribution).

You should be familiar with the basics of Internet email -- in
particular, if you don't understand the importance of envelope
addresses, you're in for a hard time.  (You could do worse than buy a
copy of Philip Hazel's book on Exim, although the second edition has not
yet been published as I write this.)  You should also acquaint yourself
with the documentation for the C local_scan() API; see Chapter 37 of the
Exim 4 spec.  Finally, you should already know how to program in Python.


Overview
---------

Once you have built and installed an Exim binary with elspy linked in,
every message received by Exim will be processed by the C
``local_scan()`` function provided by elspy (in ``elspy.c``).  This
function does the following:

1) Initialize the embedded Python interpreter.  (This only happens
   once per process, so if the same SMTP server process handles
   multiple messages, it only initializes Python once.  Generally,
   though, Exim handles one message per process.)

2) Import the ``elspy`` Python module.  If this fails, your elspy
   installation is incomplete or broken.  In that case, elspy
   logs the error (with traceback) to Exim's main log and gives
   up, accepting the message.

3) Import the ``exim_local_scan`` Python module, which you must supply
   and install.  If this fails, then you haven't fulfilled your side
   of the bargain; elspy again logs the error and traceback and accepts
   the message.

   Your ``exim_local_scan`` module should provide a function called
   ``local_scan()``, which is ultimately called by elspy's C
   ``local_scan()``.

   (On servers I maintain, I make ``site-packages/exim_local_scan.py`` a
   symlink to ``/etc/exim/local_scan.py``, so all Exim-related files are
   in the same place.  That also keeps me from having to re-install
   ``exim_local_scan.py`` to ``site-packages`` every time I make a
   change.)

4) Look for a function called ``_local_scan()``, first in the
   site-specific ``exim_local_scan`` module, and then in the ``elspy``
   package.  If this isn't found, then your elspy installation is
   broken; elspy logs the error and instructs Exim to temporarily reject
   the message.

5) Call the ``_local_scan()`` function.  The default implementation of
   ``_local_scan()``, provided by elspy's Python library (see
   ``lib/__init__.py``), then calls the ``local_scan()`` function
   provided by your ``exim_local_scan`` module.  The interface to
   this function is documented below.

   If the call to ``_local_scan()`` fails (i.e., raises an uncaught
   exception), or if the ``_local_scan()`` function does not even exist,
   elspy logs the error and instructs Exim to accept the message.  (This
   behaviour is customizable by editing elspy.c -- see the
   *_ERROR_RETURN macros.)

6) Examine the return value of ``_local_scan()``, and supply an
   appropriate return value and ``return_text`` to Exim.  


The ``exim_local_scan`` module
------------------------------

The only requirement imposed on the site-supplied ``exim_local_scan``
module is that it must provide either ``_local_scan()`` (the low-level
interface) *or* ``local_scan()`` (the high-level interface).  In the
usual case, the C ``local_scan()`` in ``elspy.c`` will call
``elspy._local_scan()`` (in ``lib/__init__.py``), which in turn calls
your ``exim_local_scan.local_scan()``.

I have yet to find a use for the low-level interface -- where the C
``local_scan()`` directly calls your ``exim_local_scan._local_scan()``
-- so I'm leaving it undocumented; it might disappear in a future
release of elspy.  If you come up with a use for it, or would like to
see it documented, bring it up on elspy-users@python.net!

Thus, for the purposes of this document, your ``exim_local_scan`` module
must provide a ``local_scan()`` function.


The ``local_scan()`` function
-----------------------------

The interface to the ``local_scan()`` function is::

  local_scan(info, headers, fd)

The parameters are:

info : EximInfo
  an instance of ``elspy.util.EximInfo``, which provides most of the Exim
  variables documented in the Exim spec in section 37.2, "Available Exim
  variables".  For example, ``info.sender_address`` is the envelope
  sender, and ``info.recipients_list`` is the list of envelope
  recipients (as a Python list of strings).  The ``EximInfo`` class
  is documented below.

headers : HeaderList
  an instance of ``elspy.util.HeaderList``, which provides a Python
  interface to the message headers as supplied by Exim.  Unlike the
  ``rfc822.Message`` and ``email.Message.Message`` classes supplied in
  Python's standard library, ``HeaderList`` treats the message headers
  as a sequence rather than a mapping.  It should be easy to convert
  ``HeaderList`` objects to either of the other two classes, but elspy
  doesn't provide code for that (yet).  The ``HeaderList`` class is also
  documented below.

fd : int
  the same as the ``fd`` argument that Exim passes to the C
  ``local_scan()``, i.e. "a file descriptor for the file that contains
  the body of the message", positioned at the start of the message
  body (which is not the start of the file!).

If ``local_scan()`` returns normally, the message is accepted.
``local_scan()`` may optionally return a string, which is passed back to
Exim via ``return_text`` in the C API, and ultimately winds up in the
Exim variable ``$local_scan_data``.

To permanently reject a message, ``local_scan()`` should raise a
``RejectMessage`` exception; to temporarily reject (defer) it, raise
``TempRejectMessage``.  For example, if you don't want to accept any
mail while the moon is full::

  from elspy import TempRejectMessage

  def local_scan (info, headers, fd):
      if is_moon_full():
          raise TempRejectMessage("sorry, can't accept mail -- moon full")

(The implementation of ``is_moon_full()`` is left as an exercise for the
reader.)

If you wish to refuse mail from people whose email address (in the
"From" header) has a local part of ``bob``:

  from elspy import RejectMessage

  def local_scan (info, headers, fd):
      fromhdr = headers.get("from")
      if fromhdr and fromhdr.lower().find("bob@") != -1:
          raise RejectMessage("sorry, we don't serve folks named bob here")

Note that if you don't supply an error message to ``RejectMessage``,
Exim will use the uninformative default ``Administrative prohibition``;
with ``TempRejectMessage``, the default is ``Temporary local problem``.

Finally, if you wish to short-circuit the rest of your ``local_scan()``
function and immediately accept a message, you can raise AcceptMessage:

  from elspy import AcceptMessage

  def local_scan (info, headers, fd):
      if "postmaster@my.domain" in info.recipients_list:
          raise AcceptMessage

As with the return value, if you supply a string to ``AcceptMessage``,
it will wind up in Exim's ``$local_scan_data`` variable.


The ``EximInfo`` class
----------------------

Exim provides a number of global variables to the C ``local_scan()``
function that provide useful information about the message currently
being processed.  Most of these variables are bundled up into an
``EximInfo`` object for the Python ``local_scan()`` function -- a few
are specific to C data structures that are made available in other ways
to Python code.  The attributes of the ``EximInfo`` object are:

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

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

interface_port : int
  The port on which this message was received.

received_protocol : string
  The name of the protocol by which the message was received.

recipients_list : [string] (ie., list of strings)
  The list of accepted recipients.  You can add, remove, or replace
  recipients simply by manipulating this list in the usual way;
  elspy reconstructs Exim's ``recipients_list`` structure based
  on the state of ``info.recipients_list`` when ``local_scan()``
  returns.

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

sender_host_authenticated : 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 : string
  The name of the sending host, if known.

sender_host_port : int
  The port on the sending host.


The ``HeaderList`` class
------------------------

The message headers are supplied to your Python ``local_scan()``
function as an instance of ``elspy.util.HeaderList``.  This class
provides a mostly sequence-like interface to the headers of an email
message:

``len(headers)`` : int
  Return the number of headers in the message.

``headers[i]`` : (string, string)
  Return the i'th header as a pair of strings.  Eg. if the
  seventh header line is ::

    From: Greg Ward <gward@python.net>

  then ``headers[6]`` (note the zero-based index) will be ::

    ("From", " Greg Ward <gward@python.net>")

  Note that whitespace is preserved *exactly*, so most header values
  will start with whitespace (because most header lines have whitespace
  after the colon).

``headers.has_header(name : string)`` : bool
  Return true if there is a header named ``name`` (case insensitive)
  in the header list.

``headers.get_first(name : string, default = None)`` : string
  Return the value of the first header named ``name``, or ``default``
  if no such header exists in the header list.

  ``headers.get()`` is an alias for ``headers.get_first()``.

``headers.get_last(name : string, default = None)`` : string
  Return the value of the last header named ``name``, or ``default``
  if no such header exists in the header list.
  
``headers.get_all(name : string)`` : [string]
  Return the values of all headers named ``name``, or an empty list
  if no such header exists in the header list.

``headers.add(name : string, value : string)``
  Add a header line to the header list.  If `value` does not start
  with whitespace (space or tab), adds a single space, so calling
  this ::

    headers.add("X-My-Header", "hello there!")

  adds this header line::

    X-My-Header: hello there!

  Unlike in the C ``local_scan()`` API, you don't need to supply the
  trailing newline in the header value.

``headers.as_string()`` : string
  Return the complete header list as a single string (with headers
  separated by newlines).

``headers.write(file = sys.stdout)``
  Write the complete header list to ``file`` (any file-like object
  that has a ``write()`` method will do).

Calling ``headers.add()`` updates Exim's internal header list to reflect
the new header line.  There's currently no way to remove or replace
header lines -- bring it up on elspy-users@python.net if you'd like to
see that feature added.  (I haven't needed it yet; when I do, I'll add
it!)


$Id: API.txt,v 1.3 2002/10/22 22:54:42 gward Exp $
