Milter library for Clojure
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
src/demyjtify
test/demyjtify
.gitignore
README.org
project.clj

README.org

DEMYJTIFY: a milter library in Clojure

This is a Clojure library to implement milters. A milter is a Sendmail filter (hence the contraction); a daemon program that extends and augments the Sendmail functionality and implements features that are not provided by Sendmail itself, such as spam filtering, virus protection, mail archiving, mailing lists etc. Matter of fact, much of the logic behind Sendmail routing and access control could, in fact, be off loaded to a milter or a composition of milters.

Milters are usually C programs linked to the libmilter library, which comes with Sendmail. Interfacing to such library is not always an option, especially for many Lisp systems.

The libmilter library implements the milter protocol, the (de)serialisation of the data and the multi-threading. This is what demyjtify does as well, in a more lispy style.

How it works

The program calls start-milter passing a port number and a function. The milter library binds a socket to that port and waits for Sendmail connections.

For each connection, the milter library spawns a new thread and calls the callback function that was provided to start-milter, passing a context map. The map, at this point in time, is populated by just one value, the socket. The callback, in turn, must return a context map that usually contains further state data and milter options such as the event callbacks and various protocol options.

Each event callback is itself a function that accepts an event map and a context map, and returns a context map (not necessarily the same).

On each event, received from Sendmail, the library calls the relevant handler (callback). The handler can send back action request(s) to Sendmail. Some events don’t require any answer/action, though.

Installation

Add the dependency to your lein project like this:

[fourtytoo/demyjtify "0.1.0-SNAPSHOT"]

or whatever version happens to be the one you picked.

If demyjtify project is not listed on Clojars you’ll have to download it and install it yourself. Here is how.

Clone the library from GitHub

cd somewhere/outside/your/project
git clone git@github.com:fourtytoo/demyjtify.git

and install it

cd demyjtify
lein install

Add the dependency to your project. Check how to do it on the leiningen [page](https://github.com/technomancy/leiningen/blob/stable/doc/TUTORIAL.md#checkout-dependencies). But that basically boils down to creating a checkouts directory in the root of your project

cd your/project/root/directory
mkdir checkouts

and inside it, creating a symbolic link to demyjtify’s root directory

cd checkouts
ln -s somewhere/outside/your/project/demyjtify .

Usage

Add demyjtify.core to your namespace like this:

(ns my-program.core
  (:require [fourtytoo.demyjtify.core :as milter]))

To use this library, all you have to do is:

  • specialise the event callbacks for all the events you care about
  • call start-milter

The event callbacks must return a new context object and, possibly, perform a milter action among those negotiated with Sendmail.

This library is stateless, so the program is responsible to save its state in the context map that is passed to and returned by the event handlers.

In principle the lifetime of a context is the same as the connection to Sendmail. The user program has to make sure to reset whatever state that is message-specific. Usually good places are the mail or the end-of-message=/=abort handlers.

start-milter is a procedure that never exits under normal circumstances. It enters a loop serving MTA connections on the specified socket. You don’t need to use start-milter, if you want to write your own server function, go ahead, but for most practical purposes it does what you need to connect to Sendmail.

The callbacks (event handlers)

The callbacks are kept in a map. The key represents the milter event type (coming from Sendmail) and the function should accept two arguments; the event and the context.

The events a milter can handle are:

  • :abort when Sendmail aborts the current message (other messages may follow)
  • :body a chunk of the message body (after the headers)
  • :connect when a client MTA establishes a connection to our Sendmail daemon
  • :data marks the beginning of the message body
  • :disconnect Sendmail wishes to disconnect but it may connect again later
  • :end-of-headers to signal the end of the email’s headers part
  • :end-of-message at the end of a message body
  • :header for each email header
  • :hello when Sendmail gets a HELO from a connected client
  • :mail when Sendmail receives a MAIL command from its client
  • :quit when Sendmail asks the milter to lay down and die
  • :recipient for each recipient on the email envelope
  • :unkown invalid SMTP command from Sendmail’s client

Beyond those above, this milter library handles internally the following events. In normal circumstances you shouldn’t bother with them:

  • :define-macro definition of symbolic values that supplement other events
  • :options negotiation of event and actions between Sendmail and the milter

The define-event-handlers helps you define the event handlers. Example:

(def byte-counter (atom 0))
(def message-counter (atom 0))

(define-event-handlers my-handlers
  (:body
   (send-action {:action :continue} context)
   (update-in context [:byte-count]
              #(+ % (count (event :data)))))
  (:mail
   (send-action {:action :continue} context)
   (assoc context :byte-count 0))
  (:abort
   (->> (assoc context :byte-count 0)
        (default-event-handler event)))
  (:end-of-message
   (swap! byte-counter
          #(+ % (context :byte-count)))
   (swap! message-counter inc)
   (println byte-counter message-counter)
   (default-event-handler event context)))

The handlers are passed in the context map, associated to the :handlers keyword.

Milter start

To start the milter you simply call start-milter and you pass the internet port and the connection callback. The callback will be called with a context map which should be augmented with additional milter options and stuff your milter might need. Example:

(defn my-program [port]
  (println "Starting server on port" port)
  (future
    (start-milter port
                  (fn [ctx]
                    (println "got MTA connection" ctx)
                    (assoc ctx :byte-count 0
                         :some-other-internal-state {:foo 1 :bar 2}
                        ;; defined above with define-event-handlers
                         :handlers my-handlers)))))

Options negotiation (events and actions)

Part of the milter protocol is the negotiation of actions and events Sendmail should expect (former) or provide (latter). A milter must declare them upfront before any actual mail processing is performed. Whereas the events are automatically deduced by demyjtify from the :handlers you provide, the actions are not. You need to specify them in the context you return to demyjtify from the connection function.

The events requested to Sendmail are those specified with the :handlers and those with the :optional-events keyword. The latter should be a subset of the :handlers. The actions requested to Sendmail are those specified with the :actions and :optional-actions keyword.

The semantics of these sets should be self explanatory; the optional actions/events are those the milter would be able to cope without (possibly with a reduced functionality) without entirely failing its purpose.

For instance:

(start-milter port
              (fn [ctx]
                (-> ctx
                    (assoc :actions #{:add-recipient})
                    (assoc :optional-actions #{:add-header}))))

In the example above the milter might need to add recipients to messages, but it can forgo adding new headers (to notify, for instance, that the envelope has been modified) if the MTA doesn’t agree on it.

After the options negotiation phase the context is updated with the agreed actions/events. The :events map entry will contain the set of events provided by the MTA, and the :actions will contain the set of actions the milter is allowed to perform.

Actions

During the protocol negotiation phase you need to fill the context map with a set of actions your milter means to use, selected from the following list:

  • :add-header
  • :change-body
  • :add-recipient
  • :delete-recipient
  • :change-header
  • :quarantine (equivalent to a “ask me another time”)
  • :change-sender

If the milter tries and performs an action that was not negotiated, a protocol error will be signalled by the MTA.

Macros

Before certain events Sendmail passes additional data to the milter. This data is in form of key-value pairs. Sendmail calls them macros. For example mail_host, _ (the connection host), rcpt_mailer, rcpt_host, etc.

A milter may access these values with the get-macro function, passing the current context and the macro name as a string. Example:

(let [host (get-macro ctx "_")]
  (println "Got connection from" host))

In a :recipient handler it may be used like this:

(defn my-recipient-event-handler (event context)
  (assoc context :my-recipients
         (cons {:address (extract-mail-address (event :address))
                :mailer (get-macro context "rcpt_mailer")
                :host (get-macro context "rcpt_host")}
               (context :my-recipients))))

Sendmail configuration

To install a milter in Sendmail, in /etc/mail/sendmail.mc, you have to add a line like this:

INPUT_MAIL_FILTER(`filter2', `S=inet:20025@localhost, F=T')

and compile the .mc into a .cf file:

cd /etc/mail
make
make install restart

Then make sure you use the same address in the call of start-milter:

(start-milter 20025 my-connect-callback)

The F=T flag tells Sendmail to treat milter-related errors (ie milter not listening or crashing) as temporary. Read the Sendmail’s cf/README file if you need further details.

Sendmail does not start the milters. You have to do that yourself at boot time (anyhow, before Sendmail needs them to process a message).

See also

A simple example of use is in test/…/sample.clj

The following pages could be useful to understand what a milter is and what it does:

This work is derived from the Common Lisp library demyltify, which is available on GitHub at http://github.com/fourtytoo/demyltify

Gotchas

This work is based on demyltify which is in turn based on an informal description of the undocumented Sendmail-milter protocol.

Credits

Credit should be given to Todd Vierling (tv@pobox.com, tv@duh.org) for documenting the MTA/milter protocol and writing the first implementation in Perl.

License

Copyright © 2015 Walter C. Pelissero <walter@pelissero.de>

Distributed under the GNU Lesser General Public License either version 2 or (at your option) any later version.