Almost ready for receive-only mode.
[lambdamail.git] / lambdamail.scm
1 ;; Super-basic bell-and-whistle-free SMTP server.
2 ;;
3 ;; Intended for a single-user system 
4
5 (import tcp6
6         (chicken port)
7         (chicken io)
8         (chicken string)
9         (chicken pathname)
10         (chicken file)
11         (chicken time posix)
12         (chicken process-context)
13         (chicken condition)
14         srfi-1 srfi-13 matchable)
15
16 (define lambdamail-version "0.0.1")
17
18
19 (define-record config
20   host port spool-dir)
21 (define-record message to from text helo)
22 (define (make-empty-message) (make-message "" "" "" ""))
23
24
25 ;;; SMTP transactions
26 ;;
27
28 (define ((make-smtp in-port out-port config) type)
29   (if (eq? type 'get-line)
30       (read-line in-port)
31       (write-line (conc
32                    (case type
33                      ((greeting) (conc "220 " (config-host config)
34                                        " LambdaMail v" lambdamail-version))
35                      ((ok) "250 ok")
36                      ((intermediate) "354 intermediate")
37                      ((close) "221 closing transmission channel")
38                      ((not-implemented) "502 command not implemented"))
39                    "\r") out-port)))
40
41
42 ;;; Server initialization
43 ;;
44
45 (define (run-server config)
46   (set-buffering-mode! (current-output-port) #:line)
47   (let ((listener (tcp-listen (config-port config) 10 "::")))
48     (print "LambdaMail v" lambdamail-version
49            " listening on port " (config-port config) " ...")
50     (print "(Host name: " (config-host config)
51            ", Spool dir: " (config-spool-dir config) ")")
52     (server-loop listener config)))
53
54
55 ;;; Main server loop
56 ;;
57
58 (define (server-loop listener config)
59   (let-values (((in-port out-port) (tcp-accept listener)))
60     (let-values (((local-ip remote-ip) (tcp-addresses in-port)))
61       (print "Accepted connection from " remote-ip " on " (seconds->string)))
62     (condition-case
63         (let ((smtp (make-smtp in-port out-port config)))
64           (smtp 'greeting)
65           (process-smtp smtp config))
66       (o (exn)
67          (print-error-message o)))
68     (print "Terminating connection.")
69     (close-input-port in-port)
70     (close-output-port out-port)
71     (server-loop listener config)))
72
73 ;;; SMTP processing loop
74 ;;
75
76 (define (process-smtp smtp config)
77   (let loop ((msg (make-empty-message))
78              (line-orig (smtp 'get-line)))
79     (if (string? line-orig)
80         (let ((line (string-downcase line-orig)))
81           (print "got " line)
82           (cond
83            ((string-prefix? "helo" line)
84             (message-helo-set! msg (string-drop line (string-length "helo")))
85             (smtp 'ok)
86             (loop msg (smtp 'get-line)))
87            ((string-prefix? "mail from:" line)
88             (message-from-set! msg (string-drop line (string-length "mail from:")))
89             (smtp 'ok)
90             (loop msg (smtp 'get-line)))
91            ((string-prefix? "rcpt to:" line)
92             (message-to-set! msg (string-drop line (string-length "rcpt to:")))
93             (smtp 'ok)
94             (loop msg (smtp 'get-line)))
95            ((string-prefix? "data" line)
96             (smtp 'intermediate)
97             (let text-loop ((text-line (smtp 'get-line))
98                             (text ""))
99               (if (string=? "." text-line)
100                   (message-text-set! msg text)
101                   (text-loop (smtp 'get-line)
102                              (conc text text-line "\n"))))
103             (deliver-message msg config)
104             (smtp 'ok)
105             (loop (make-empty-message)
106                   (smtp 'get-line)))
107            ((string-prefix? "quit" line)
108             (smtp 'close)
109             'done)
110            ((string=? "" line)
111             (loop msg (smtp 'get-line)))
112            (else
113             (smtp 'not-implemented)
114             (loop msg (smtp 'get-line)))))
115         'done)))
116
117
118 ;;; Message delivery
119 ;;
120
121 (define (get-to-addresses config)
122   (map (lambda (p) (cons
123                     (conc "<" (car p) "@" (config-host config) ">")
124                     (cdr p)))
125        (map (lambda (file) (cons (pathname-file file) file))
126             (glob (conc (config-spool-dir config) "/*")))))
127
128 (define (remove-angle-brackets addr)
129   (let ((left-idx (substring-index "<" addr))
130         (right-idx (substring-index ">" addr)))
131     (substring addr (+ left-idx 1) right-idx)))
132
133 (define (deliver-message msg config)
134   (let ((dest (assoc (message-to msg) (get-to-addresses config))))
135     (if dest
136         (begin
137           (with-output-to-file (cdr dest)
138             (lambda ()
139               (print "\nFrom " (remove-angle-brackets (message-from msg)))
140               (print (message-text msg)))
141             #:append)
142           (print "Message DELIVERED:"))
143         (print "Message REJECTED:"))
144     (print " * From: " (message-from msg))
145     (print " * To: " (message-to msg))))
146
147
148 ;;; Command line argument parsing
149 ;;
150
151 (define (print-usage progname)
152   (print "Usage: " progname " hostname [port [spooldir]]"))
153
154 (define (main)
155   (let ((progname (pathname-file (car (argv))))
156         (args (cdr (argv)))
157         (config (make-config "" 25 "/var/spool/mail")))
158     (if (null? args)
159         (print-usage progname)
160         (begin
161           (config-host-set! config (car args))
162           (unless (null? (cdr args))
163             (config-port-set! config (string->number (cadr args)))
164             (unless (null? (cddr args))
165               (config-spool-dir-set! (caddr args))))
166           (run-server config)))))
167
168 (main)
169
170 ;; (run-server (make-config "thelambdalab.xyz" 2525 "/var/spool/mail"))