]> gitweb.factorcode.org Git - factor.git/blob - basis/smtp/smtp.factor
basis: ERROR: changes.
[factor.git] / basis / smtp / smtp.factor
1 ! Copyright (C) 2007, 2009 Elie CHAFTARI, Dirk Vleugels,
2 ! Slava Pestov, Doug Coleman, Daniel Ehrenberg.
3 ! See http://factorcode.org/license.txt for BSD license.
4 USING: accessors arrays assocs base64 calendar calendar.format
5 classes combinators debugger fry io io.crlf io.encodings
6 io.encodings.ascii io.encodings.binary io.encodings.iana
7 io.encodings.string io.encodings.utf8 io.sockets
8 io.sockets.secure io.timeouts kernel logging make math.order
9 math.parser namespaces prettyprint random sequences sets
10 splitting strings words ;
11 IN: smtp
12
13 TUPLE: smtp-config domain server tls? { read-timeout duration } auth ;
14
15 SINGLETON: no-auth
16
17 TUPLE: plain-auth username password ;
18 C: <plain-auth> plain-auth
19
20 : <smtp-config> ( -- smtp-config )
21     smtp-config new ; inline
22
23 : default-smtp-config ( -- smtp-config )
24     <smtp-config>
25         "localhost" 25 <inet> >>server
26         1 minutes >>read-timeout
27         no-auth >>auth ; inline
28
29 LOG: log-smtp-connection NOTICE
30
31 : with-smtp-connection ( quot -- )
32     smtp-config get server>>
33     dup log-smtp-connection
34     ascii [
35         smtp-config get
36         [ [ host-name or ] change-domain drop ]
37         [ read-timeout>> timeouts ] bi
38         call
39     ] with-client ; inline
40
41 : with-smtp-config ( quot -- )
42     [ \ smtp-config get-global clone \ smtp-config ] dip
43     '[ _ with-smtp-connection ] with-variable ; inline
44
45 TUPLE: email
46     { from string }
47     { to array }
48     { cc array }
49     { bcc array }
50     { subject string }
51     { content-type string initial: "text/plain" }
52     { encoding word initial: utf8 }
53     { body string } ;
54
55 : <email> ( -- email ) email new ; inline
56
57 <PRIVATE
58
59 : command ( string -- ) write crlf flush ;
60
61 \ command DEBUG add-input-logging
62
63 : helo ( -- ) "EHLO " host-name append command ;
64
65 : start-tls ( -- ) "STARTTLS" command ;
66
67 ERROR: bad-email-address email ;
68
69 : validate-address ( string -- string' )
70     #! Make sure we send funky stuff to the server by accident.
71     dup "\r\n>" intersects?
72     [ throw-bad-email-address ] when ;
73
74 : mail-from ( fromaddr -- )
75     validate-address
76     "MAIL FROM:<" ">" surround command ;
77
78 : rcpt-to ( to -- )
79     validate-address
80     "RCPT TO:<" ">" surround command ;
81
82 : data ( -- )
83     "DATA" command ;
84
85 : send-body ( email -- )
86     binary encode-output
87     [ body>> ] [ encoding>> ] bi encode >base64-lines write
88     ascii encode-output crlf
89     "." command ;
90
91 : quit ( -- )
92     "QUIT" command ;
93
94 LOG: smtp-response DEBUG
95
96 : multiline? ( response -- ? )
97     3 swap ?nth CHAR: - = ;
98
99 : (receive-response) ( -- )
100     read-crlf
101     [ , ]
102     [ smtp-response ]
103     [ multiline? [ (receive-response) ] when ]
104     tri ;
105
106 TUPLE: response code messages ;
107
108 : <response> ( lines -- response )
109     [ first 3 head string>number ] keep response boa ;
110
111 : receive-response ( -- response )
112     [ (receive-response) ] { } make <response> ;
113
114 ERROR: smtp-error response ;
115
116 M: smtp-error error.
117     "SMTP error (" write dup class-of pprint ")" print
118     response>> messages>> [ print ] each ;
119
120 ERROR: smtp-server-busy < smtp-error ;
121 ERROR: smtp-syntax-error < smtp-error ;
122 ERROR: smtp-command-not-implemented < smtp-error ;
123 ERROR: smtp-bad-authentication < smtp-error ;
124 ERROR: smtp-mailbox-unavailable < smtp-error ;
125 ERROR: smtp-user-not-local < smtp-error ;
126 ERROR: smtp-exceeded-storage-allocation < smtp-error ;
127 ERROR: smtp-bad-mailbox-name < smtp-error ;
128 ERROR: smtp-transaction-failed < smtp-error ;
129
130 : check-response ( response -- )
131     dup code>> {
132         { [ dup { 220 235 250 221 354 } member? ] [ 2drop ] }
133         { [ dup 400 499 between? ] [ drop throw-smtp-server-busy ] }
134         { [ dup 500 = ] [ drop throw-smtp-syntax-error ] }
135         { [ dup 501 = ] [ drop throw-smtp-command-not-implemented ] }
136         { [ dup 500 509 between? ] [ drop throw-smtp-syntax-error ] }
137         { [ dup 530 539 between? ] [ drop throw-smtp-bad-authentication ] }
138         { [ dup 550 = ] [ drop throw-smtp-mailbox-unavailable ] }
139         { [ dup 551 = ] [ drop throw-smtp-user-not-local ] }
140         { [ dup 552 = ] [ drop throw-smtp-exceeded-storage-allocation ] }
141         { [ dup 553 = ] [ drop throw-smtp-bad-mailbox-name ] }
142         { [ dup 554 = ] [ drop throw-smtp-transaction-failed ] }
143         [ drop throw-smtp-error ]
144     } cond ;
145
146 : get-ok ( -- ) receive-response check-response ;
147
148 GENERIC: send-auth ( auth -- )
149
150 M: no-auth send-auth drop ;
151
152 : plain-auth-string ( username password -- string )
153     [ "\0" prepend ] bi@ append utf8 encode >base64 >string ;
154
155 M: plain-auth send-auth
156     [ username>> ] [ password>> ] bi plain-auth-string
157     "AUTH PLAIN " prepend command get-ok ;
158
159 : auth ( -- ) smtp-config get auth>> send-auth ;
160
161 : encode-header ( string -- string' )
162     dup aux>> [
163         utf8 encode >base64
164         "=?utf-8?B?" "?=" surround
165     ] when ;
166
167 ERROR: invalid-header-string string ;
168
169 : validate-header ( string -- string' )
170     dup "\r\n" intersects?
171     [ throw-invalid-header-string ] when ;
172
173 : write-header ( key value -- )
174     [ validate-header write ]
175     [ ": " write validate-header encode-header write ] bi* crlf ;
176
177 : write-headers ( assoc -- )
178     [ write-header ] assoc-each ;
179
180 : message-id ( -- string )
181     [
182         "<" %
183         64 random-bits #
184         "-" %
185         gmt timestamp>micros #
186         "@" %
187         smtp-config get domain>> [ host-name ] unless* %
188         ">" %
189     ] "" make ;
190
191 : extract-email ( recepient -- email )
192     ! This could be much smarter.
193     " " split1-last swap or "<" ?head drop ">" ?tail drop ;
194
195 : email-content-type ( email -- content-type )
196     [ content-type>> ] [ encoding>> encoding>name ] bi "; charset=" glue ;
197
198 : email>headers ( email -- assoc )
199     [
200         now timestamp>rfc822 "Date" ,,
201         message-id "Message-Id" ,,
202         "1.0" "MIME-Version" ,,
203         "base64" "Content-Transfer-Encoding" ,,
204         {
205             [ from>> "From" ,, ]
206             [ to>> ", " join "To" ,, ]
207             [ cc>> ", " join [ "Cc" ,, ] unless-empty ]
208             [ subject>> "Subject" ,, ]
209             [ email-content-type "Content-Type" ,, ]
210         } cleave
211     ] H{ } make ;
212
213 : (send-email) ( headers email -- )
214     [
215         get-ok
216         helo get-ok
217         smtp-config get tls?>> [ start-tls get-ok send-secure-handshake ] when
218         auth
219         dup from>> extract-email mail-from get-ok
220         dup to>> [ extract-email rcpt-to get-ok ] each
221         dup cc>> [ extract-email rcpt-to get-ok ] each
222         dup bcc>> [ extract-email rcpt-to get-ok ] each
223         data get-ok
224         swap write-headers
225         crlf
226         send-body get-ok
227         quit get-ok
228     ] with-smtp-connection ;
229
230 PRIVATE>
231
232 : send-email ( email -- )
233     [ email>headers ] keep (send-email) ;