1
> {-# LANGUAGE DeriveDataTypeable, PatternGuards #-}
2
3
A Walk Through "Config.hs"
4
==========================
5
6
:Author: Peter Simons <simons@cryp.to>
7
:Date:   2008-02-20
8
:Note:   This text is *nowhere* near being complete.
9
10
.. contents::
11
12
Welcome To The Real World
13
-------------------------
14
15
The purpose of this document is to provide a hands-on
16
introduction to the Postmaster ESMTP server. Naturally, it
17
is written as a literate Haskell `source code`_, so you can
18
load it into ``ghci`` and run any of the examples in the
19
interactive development environment while reading the text.
20
Note that Postmaster must be linked to the system libraries
21
``-ladns -lcrypto``, so you'll have to start the interpreter
22
with those flags given on the command-line. (If you run
23
``ghci`` from Emacs this should be configured
24
automatically.)
25
26
I have decided against explaining the internals of the
27
daemon. I'll write this text treating the functions
28
Postmaster provides just like any other Haskell library. I
29
think it is better to do it this way because you, as the
30
user, probably don't care how Postmaster works. You only
31
care how to configure a real bad-ass MTA. So I'll do just
32
that and refer you to the `reference documentation`_ for
33
the details.
34
35
::
36
37
> module Main where
38
>
39
> import System.Time
40
> import System.Posix.User
41
> import Network.Socket ( SockAddr(..) )
42
> import Data.Char
43
> import Postmaster hiding ( main )
44
45
> ioBufferSize :: Capacity
46
> ioBufferSize = 1024
47
48
> port :: PortID
49
> port = PortNumber 2525
50
51
> run :: EventT -> IO ()
52
> run f = main' ioBufferSize port f
53
54
You have a working SMTP daemon now. Just start it with ``run
55
id`` and ``telnet`` to the server::
56
57
  $ telnet localhost 2525
58
  Trying 127.0.0.1...
59
  Connected to localhost.
60
  Escape character is '^]'.
61
  220 peti.cryp.to Postmaster ESMTP Server
62
  NOOP
63
  250 Massive system failure. Just kidding ... OK.
64
  QUIT
65
  221 peti.cryp.to closing connection
66
  Connection closed by foreign host.
67
68
The default configuration will ...
69
70
- accept every HELO and EHLO command,
71
- accept every MAIL command,
72
- refuse every RCPT command;
73
- thus, refuse DATA commands for lack of recipients.
74
75
Writing Event Handlers
76
''''''''''''''''''''''
77
78
::
79
80
> debugEH :: String -> EventT
81
> debugEH name f e = do
82
>   yell (StartEventHandler name e)
83
>   r@(Reply rc _) <- f e
84
>   yell (EventHandlerResult name e rc)
85
>   return r
86
87
The constructors ``StartEventHandler`` etc. are defined by
88
Postmaster; it's one of many ``LogEvents`` you may trigger
89
whenever you think it's appropriate. By wrapping any event
90
handler with this combinator, we can trace its input and
91
output values. With two little helper functions, we can try
92
it out right away::
93
94
> mainDebug :: IO ()
95
> mainDebug = run (debugEH "default")
96
97
If you speak with Postmaster now, you should notice the new
98
log messages that show up on your ``ghci`` terminal (and in
99
the system log file you've configured for ``syslog(3)``)::
100
101
  SID 1: StartEventHandler "default" Greeting
102
  SID 1: EventHandlerResult "default" Greeting 220
103
104
105
Standard Unix Configuration
106
'''''''''''''''''''''''''''
107
108
Let's configure a real MTA that actually does something. As
109
a simple example, let us reimplement the way MTAs have
110
worked traditionally under Unix: (1) The MTA has a list of
111
"local hostnames". Any recipient which is not in one of
112
these domains is refused. (2) Recipients *in* the local
113
domains are delivered to the Unix user with the same name as
114
the local part. (3) All system users are valid e-mail
115
recipients. (4) Everything else needs an manual entry a.k.a.
116
"alias".
117
118
One straightforward way to implement this scheme is by
119
splitting these requirements into the three functions: The
120
first one checks whether the address of the ``AddRcptTo``
121
event is a local hostname; the second checks the system
122
users database, and the third one unconditionally delivers
123
to the local-part. By wrapping these functions around each
124
other in the right order, we get exactly the behavior
125
described above.
126
127
The implementation is trivial::
128
129
> localHosts :: [HostName] -> EventT
130
> localHosts lhosts _ (AddRcptTo (Mailbox _ _ host)) = do
131
>   if (map toLower host) `elem` lhosts
132
>      then say 2 5 0 "TODO: We accept everything right now"
133
>      else say 5 5 3 "unknown recipient"
134
> localHosts _ f e = f e
135
136
For it to work, the list you give the function must contain
137
the local hostnames in all lower-case, obviously. Note that
138
this combinator works differently than the earlier ones: It
139
doesn't use ``f`` as a fallback but *guards* access to ``f``!
140
141
Our database lookup isn't complicated either: [5]_ ::
142
143
> exposePasswd :: EventT
144
> exposePasswd f e@(AddRcptTo (Mailbox _ lpart _)) = do
145
>   pwdentry <- liftIO (getUserEntryForName lpart)
146
>   if userName pwdentry == lpart   -- kludge for ghc
147
>      then f e
148
>      else say 5 5 3 "unknown recipient"
149
> exposePasswd f e = f e
150
151
Another function that guards access to ``f`` on the
152
``AddRcptTo`` event. And our local mailer is::
153
154
 TODO: broken with spooler
155
156
 | localProcmail :: EventT
157
 | localProcmail _ (AddRcptTo mbox@(Mailbox _ lpart _)) =
158
 |   procmail [mbox] lpart []
159
 | localProcmail f e = f e
160
161
Done. ::
162
163
> stdConfig :: EventT
164
> stdConfig =
165
>   localHosts myHostNames . exposePasswd -- TODO: . localProcmail
166
167
The code is point-free, so it must be good. Now edit the
168
list of local hostnames to suit your system's setup ... ::
169
170
> myHostNames :: [HostName]
171
> myHostNames = [ "localhost"
172
>               , "change-me.example.org"
173
>               ]
174
175
and run your MTA::
176
177
> stdMTA :: IO ()
178
> stdMTA = run stdConfig
179
180
 | runStdMTA :: [String] -> IO ()
181
 | runStdMTA = runTest stdConfig
182
183
A good test session should be::
184
185
> stdTest :: [String]
186
> stdTest =
187
>   [ "EHLO [127.0.0.1]"
188
>   , "MAIL FROM:<\"foo\\\".bar\"@example.net>"
189
>   , "RCPT TO:<non.existent@localhost>"
190
>   , "RCPT TO:<root@example.com>"
191
>   , "RCPT TO:<root@localhost>"
192
>   , "DATA"
193
>   , "From: simons@cryp.to (Peter Simons)"
194
>   , "Subject: Testing Postmaster"
195
>   , ""
196
>   , "Won't work anyway."
197
>   , "."
198
>   ]
199
200
If you run ``testStdMTA stdTest``, you'll most likely find
201
the e-mail in the file ``/var/mail/root`` now. Procmail,
202
which we used for local delivery, doesn't care about any
203
``/etc/mail/aliases`` you might have. Which is good, because
204
we want to determine the aliases in Postmaster, not
205
somewhere else.
206
207
Aliases and Exploders
208
'''''''''''''''''''''
209
210
Aliases ... phew. That ought to be difficult? ::
211
212
> alias :: [(Mailbox, Mailbox)] -> EventT
213
> alias theDB f e
214
>   | AddRcptTo mbox <- e
215
>   , Just mbox' <- lookup mbox theDB
216
>       = trigger (AddRcptTo mbox')
217
>   | otherwise  = f e
218
219
Why do we need ``trigger``? Instead of that definition, we
220
could equally well have used::
221
222
  alias theDB f e
223
    | ...
224
        = alias theDB f (AddRcptTo mbox')
225
226
The semantics differ insofar as that this variant will
227
properly recurse, but it will bypass the access checks that
228
might have run *before* ``alias`` was even called. We don't
229
know how deeply nested we are in the event transformer
230
chain! The function ``triggers`` allows us to call the
231
entire chain from the beginning.
232
233
By-passing the checks might be what you want in some cases,
234
actually. But I'd rather define an explicit handler for
235
addresses like that. ``alias`` rewrites addresses; nothing
236
more, nothing less. Here is a short demo function::
237
238
 | runAliasTest :: IO ()
239
 | runAliasTest = runTest (myalias . stdConfig) stdTest
240
 |   where
241
 |   lhs     = read "non.existent@localhost"
242
 |   rhs     = read "root@localhost"
243
 |   myalias = alias [(lhs,rhs)]
244
245
You will have noticed that the mechanism doesn't look like
246
the usual aliases file. It maps addresses one-to-one, not
247
one-to-many. In fact, it is more similar to Sendmail's
248
``virtusertable``. because our left handside of the rewrite
249
is a full e-mail address, not just a local part. If we want
250
to have one-to-many mappings, this a simple way to do it::
251
252
> explode :: Mailbox -> Smtpd SmtpReply -> EventT
253
> explode lhs mkRhs f e
254
>   | AddRcptTo mbox <- e,  lhs == mbox
255
>                = mkRhs
256
>   | otherwise  = f e
257
258
 | runExploderTest :: IO ()
259
 | runExploderTest = runTest (expl . stdConfig) stdTest
260
 |   where
261
 |   expl = explode (read "non.existent@localhost")
262
 |            (do shell [] "cat >/dev/null"
263
 |                shell [] "cat >/dev/null"
264
 |                -- add more
265
 |                say 2 5 0 "great")
266
267
Cooler Event Handlers
268
---------------------
269
270
The Generic Environment
271
'''''''''''''''''''''''
272
273
Which brings us to the question of how we write a stateful
274
handler then? What if we want to keep transient information
275
for a session -- or beyond the life-time of a session?
276
277
For that purpose Postmaster features two finite-map
278
environments: a global one, and a per-TCP-session one. These
279
environments work almost exactly the like Shell variables
280
under Unix do. ::
281
282
  local  :: EnvT a -> Smtpd a
283
  global :: EnvT a -> Smtpd a
284
285
Disallow Routing Addresses
286
''''''''''''''''''''''''''
287
288
::
289
290
> noRouteAddr :: EventT
291
> noRouteAddr _ (SetMailFrom (Mailbox (_:_) _ _))
292
>   = say 5 0 4 "You are kidding, right?"
293
> noRouteAddr _ (AddRcptTo   (Mailbox (_:_) _ _))
294
>   = say 5 0 4 "no source routing"
295
> noRouteAddr f e = f e
296
297
Dynamic Blacklisting
298
''''''''''''''''''''
299
300
::
301
302
> data (Typeable a) => TimeStamped a = TS ClockTime a
303
>     deriving (Typeable, Show)
304
>
305
> type Blacklist = [TimeStamped HostAddress]
306
>
307
> blacklist :: TimeDiff -> EventT
308
> blacklist ttl f e = do
309
>   r <- f e
310
>   if e /= Greeting || isFailure r then return r else do
311
>     peer <- getPeerAddr
312
>     case peer of
313
>       Nothing                       -> return r
314
>       Just (SockAddrUnix _)         -> return r
315
>       Just sa@(SockAddrInet _ addr) -> do
316
>         now <- liftIO getClockTime
317
>         let delta  = addToClockTime ttl
318
>             stale  = \(TS ts _) -> delta ts < now
319
>             clean  = reverse . dropWhile stale . reverse
320
>             expire = (\bl -> (bl,bl)) . maybe [] clean
321
>         blackl <- global (modifyVar (mkVar "blacklist") expire)
322
>         if all (\(TS _ a) -> a /= addr) blackl
323
>             then return r
324
>             else do yell (Msg (msg sa))
325
>                     say 5 5 4 "no SMTP service here"
326
>       Just (SockAddrInet6 _ _ _ _) -> return r
327
>   where
328
>   msg = showString "blacklist: refuse peer " . show
329
330
Now we need a function to add a peer to the blacklist
331
whenever we feel like it::
332
333
> ban :: Smtpd ()
334
> ban = do
335
>   peer <- getPeerAddr
336
>   case peer of
337
>     Nothing                      -> return ()
338
>     Just (SockAddrUnix _)        -> return ()
339
>     Just (SockAddrInet6 _ _ _ _) -> return ()
340
>     Just sa@(SockAddrInet _ a) -> do
341
>       yell (Msg (msg sa))
342
>       now <- liftIO getClockTime
343
>       let a'     = TS now a
344
>           append = maybe [a'] (\as -> a' : as)
345
>       global (modifyVar_ (mkVar "blacklist") append)
346
>       return ()
347
>   where
348
>   msg = showString "black-listing peer: " . show
349
350
An SMTP reply code of 221 or 421 from the event handler
351
causes Postmaster to drop the connection after the reply::
352
353
> bye :: Smtpd SmtpReply
354
> bye = do
355
>   whoami <- myHeloName
356
>   say 4 2 1 (showString whoami " Hasta la vista, baby.")
357
358
::
359
360
> impatient :: Int -> EventT
361
> impatient permFailBound f e = do
362
>   r@(Reply (Code rc _ _) _) <- f e
363
>   case rc of
364
>     PermanentFailure -> do
365
>       c <- local (tick (mkVar "permFailures"))
366
>       if c >= permFailBound
367
>          then ban >> bye
368
>          else return r
369
>     _ -> return r
370
371
> badass :: EventT
372
> badass = blacklist ttl . impatient maxPF . noRouteAddr
373
>   where
374
>   ttl   = noTimeDiff { tdMin = 30 }
375
>   maxPF = 3
376
377
The Rules Of RFC2821
378
--------------------
379
380
In all of the text I assume you are familiar with
381
[RFC2821]_. So I'll just explain a few minor details
382
concerning `how the RFC is implemented <Rfc2821.html>`_ in
383
Postmaster.
384
385
Mailboxes
386
'''''''''
387
388
The data type ``Mailbox`` is of a certain importance in this
389
text. It is defined like this::
390
391
  data Mailbox = Mailbox [String] String String
392
393
The most general e-mail address defined in the RFC has the
394
form ``<[@route,...:]user@domain>``, and ``Mailbox`` mirrors
395
that exactly. You'll find that the first field, the optional
396
routing information, is rather unpopular these days. But
397
what can I do? It is part of an e-mail address.
398
399
``Mailbox`` is an instance of ``Read`` and ``Shown``, so you
400
can use the text-representation to create mailboxes in a
401
comfortable way. Just use ``read "user@domain.tld"`` and
402
that's it. In case of mailboxes, ``read . show = id`` holds,
403
but ``show . read = id`` does *not*, because a mailbox
404
returned by ``show`` will always be enclosed in angular
405
brackets. Mailbox is also in class ``Eq``, and ``(mb ==
406
mb')`` will treat the hostname as case-insensitive, as the
407
RFC requires.
408
409
There are two special mailboxes  defined for the SMTP dialogue::
410
411
  nullPath, postmaster :: Mailbox
412
  nullPath   = Mailbox [] [] []
413
  postmaster = Mailbox [] "postmaster" []
414
415
Don't forget to do something with those. ``MAIL FROM:<>``
416
and ``RCPT TO:<postmaster>`` must always be valid commands.
417
418
SMTP Reply Codes by Function Groups
419
''''''''''''''''''''''''''''''''''''
420
421
These reply codes are suggested in the RFC. You ultimately,
422
you can do what you want because nobody cares for more than
423
the first digit anyway.
424
425
``500``
426
  Syntax error, command unrecognized
427
``501``
428
  Syntax error in parameters or arguments
429
``502``
430
  Command not implemented
431
``503``
432
  Bad sequence of commands
433
``504``
434
  Command parameter not implemented
435
``211``
436
  System status, or system help reply
437
``214``
438
  Help message
439
  (Information on how to use the receiver or the meaning of a
440
  particular non-standard command; this reply is useful only
441
  to the human user)
442
``220``
443
  <domain> Service ready
444
``221``
445
  <domain> Service closing transmission channel. See 421.
446
``421``
447
  <domain> Service not available. This may be a reply to any
448
  command if the service knows it must shut down. When the
449
  event handler returns this code (or 221), Postmaster will
450
  drop the connection after handling it.
451
``250``
452
  Requested mail action okay, completed
453
``251``
454
  User not local; will forward to <forward-path>
455
``252``
456
  Cannot VRFY user, but will accept message and attempt
457
  delivery
458
``450``
459
  Requested mail action not taken: mailbox unavailable
460
``550``
461
  Requested action not taken: mailbox unavailable
462
``451``
463
  Requested action aborted: error in processing
464
``551``
465
  User not local; please try <forward-path>
466
``452``
467
  Requested action not taken: insufficient system storage
468
``552``
469
  Requested mail action aborted: exceeded storage allocation
470
``553``
471
  Requested action not taken: mailbox name not allowed
472
  (e.g., mailbox syntax incorrect)
473
``554``
474
  Transaction failed (Or, in the case of a connection-opening
475
  response, "No SMTP service here")
476
477
Notes
478
-----
479
480
.. [1] The port-number argument doesn't have enough
481
       granularity. I'll soon change that API to expect a
482
       socket, so that you can specify on which IP address
483
       to listen, too.
484
485
.. [2] I wonder whether I should change that to ``Doc``, to
486
       allow pretty-printing. Opinions are welcome.
487
488
489
.. [3] In fact, the ``relay`` target is implemented on top
490
       of ``pipe`` at the moment. Postmaster doesn't have a
491
       mail queue yet, so it can't relay itself. (That will
492
       change.) ``relay`` uses the field ``sendmailPath``
493
       from the configuration and just pipes the message
494
       into Sendmail with appropriate arguments.
495
496
.. [4] Yes, the call to ``sed`` in ``shell`` is not nice.
497
       That will change. Postmaster does support re-writing
498
       of the data section already, I just wanted to keep
499
       the internal structure as simple as possible for the
500
       time being.
501
502
.. [5] GHC seems to have a bug in ``getUserEntryForName``
503
       which causes it to return an incorrect entry when the
504
       requested one doesn't exist. Until that's fixed, we
505
       use the comparison for equality of the user names to
506
       determine success, rather than catching the exception
507
       we were supposed to get in case of failure.
508
509
.. [6] Mostly because I'll replace it with
510
     ``Data.Dynamic.Dynamic`` soon anyway.
511
512
Change me::
513
514
> main :: IO ()
515
> main = run (badass . stdConfig)
516
517
References
518
----------
519
520
.. [RFC2821] Simple Mail Transfer Protocol: http://www.faqs.org/rfcs/rfc2821.html
521
522
.. [Postmaster] Homepage: http://postmaster.cryp.to/
523
524
.. [Haskell] The Haskell Homepage: http://www.haskell.org/
525
526
.. [GHC] The Glorious Haskell Compiler: http://www.haskell.org/ghc/
527
528
.. [Sendmail] Sendmail Homepage: http://sendmail.org/
529
530
.. [Procmail] Procmail Homepage: http://www.procmail.org/
531
532
.. _source code: http://postmaster.cryp.to/tutorial.lhs
533
534
.. _reference documentation: index.html
535
536
.. _events: Rfc2821.html#t%3AEvent
537
538
539
.. ----- Configure Emacs -----
540
..
541
.. Local Variables: ***
542
.. haskell-program-name: "ghci -ladns -lcrypto" ***
543
.. End: ***