Add support for AUTH PLAIN to smtp library

Update documentation
The smtp.server used for testing now starts on a random port instead of hard-coding 4321
db4
Slava Pestov 2008-11-30 10:12:08 -06:00
parent 6b0a477d3d
commit dce74a6915
4 changed files with 105 additions and 40 deletions

View File

@ -2,12 +2,12 @@
! See http://factorcode.org/license.txt for BSD license.
USING: combinators kernel prettyprint io io.timeouts
sequences namespaces io.sockets continuations calendar
io.encodings.ascii io.streams.duplex destructors ;
io.encodings.ascii io.streams.duplex destructors
locals concurrency.promises threads accessors ;
IN: smtp.server
! Mock SMTP server for testing purposes.
! Usage: 4321 mock-smtp-server
! $ telnet 127.0.0.1 4321
! Trying 127.0.0.1...
! Connected to localhost.
@ -62,13 +62,16 @@ SYMBOL: data-mode
]
} cond nip [ process ] when ;
: mock-smtp-server ( port -- )
"Starting SMTP server on port " write dup . flush
"127.0.0.1" swap <inet4> ascii <server> [
accept drop [
1 minutes timeouts
"220 hello\r\n" write flush
process
global [ flush ] bind
] with-stream
] with-disposal ;
:: mock-smtp-server ( promise -- )
#! Store the port we are running on in the promise.
[
"127.0.0.1" 0 <inet4> ascii <server> [
dup addr>> port>> promise fulfill
accept drop [
1 minutes timeouts
"220 hello\r\n" write flush
process
global [ flush ] bind
] with-stream
] with-disposal
] in-thread ;

View File

@ -5,20 +5,49 @@ io.sockets strings calendar ;
IN: smtp
HELP: smtp-domain
{ $description "The name of the machine that is sending the email. This variable will be filled in by the " { $link host-name } " word if not set by the user." } ;
{ $var-description "The name of the machine that is sending the email. This variable will be filled in by the " { $link host-name } " word if not set by the user." } ;
HELP: smtp-server
{ $description "Holds an " { $link inet } " object with the address of an SMTP server." } ;
{ $var-description "Holds an " { $link inet } " object with the address of an SMTP server." } ;
HELP: smtp-read-timeout
{ $description "Holds an " { $link duration } " object that specifies how long to wait for a response from the SMTP server." } ;
{ $var-description "Holds a " { $link duration } " object that specifies how long to wait for a response from the SMTP server." } ;
HELP: esmtp?
{ $description "Set true by default, determines whether the SMTP client is using the Extended SMTP protocol." } ;
HELP: smtp-auth
{ $var-description "Holds either " { $link no-auth } " or an instance of " { $link plain-auth } ", specifying how to authenticate with the SMTP server. Set to " { $link no-auth } " by default." } ;
HELP: no-auth
{ $class-description "If the " { $link smtp-auth } " variable is set to this value, no authentication will be performed." } ;
HELP: plain-auth
{ $class-description "If the " { $link smtp-auth } " variable is set to this value, plain authentication will be performed, with the username and password stored in the " { $slot "username" } " and " { $slot "password" } " slots of the tuple sent to the server as plain-text." } ;
HELP: <plain-auth> ( username password -- plain-auth )
{ $values { "username" string } { "password" string } { "plain-auth" plain-auth } }
{ $description "Creates a new " { $link plain-auth } " instance." } ;
HELP: with-smtp-connection
{ $values { "quot" quotation } }
{ $description "Connects to an SMTP server stored in " { $link smtp-server } " and calls the quotation." } ;
{ $description "Connects to an SMTP server stored in " { $link smtp-server } " and calls the quotation." }
{ $notes "This word is used to implement " { $link send-email } " and there is probably no reason to call it directly." } ;
HELP: email
{ $class-description "An e-mail. E-mails have the following slots:"
{ $table
{ { $slot "from" } "The sender of the e-mail. An e-mail address." }
{ { $slot "to" } "The recipients of the e-mail. A sequence of e-mail addresses." }
{ { $slot "cc" } "Carbon-copy. A sequence of e-mail addresses." }
{ { $slot "bcc" } "Blind carbon-copy. A sequence of e-mail addresses." }
{ { $slot "subject" } " The subject of the e-mail. A string." }
{ { $slot "body" } " The body of the e-mail. A string." }
}
"The " { $slot "from" } " and " { $slot "to" } " slots are required; the rest are optional."
$nl
"An e-mail address is a string in one of the following two formats:"
{ $list
{ $snippet "joe@groff.com" }
{ $snippet "Joe Groff <joe@groff.com>" }
} } ;
HELP: <email>
{ $values { "email" email } }
@ -26,9 +55,9 @@ HELP: <email>
HELP: send-email
{ $values { "email" email } }
{ $description "Sends an " { $link email } " object to an STMP server stored in the " { $link smtp-server } " variable. The required slots are " { $slot "from" } " and " { $slot "to" } "." }
{ $description "Sends an e-mail." }
{ $examples
{ $unchecked-example "USING: accessors smtp ;"
{ $code "USING: accessors smtp ;"
"<email>"
" \"groucho@marx.bros\" >>from"
" { \"chico@marx.bros\" \"harpo@marx.bros\" } >>to"
@ -42,10 +71,20 @@ HELP: send-email
} ;
ARTICLE: "smtp" "SMTP client library"
"Configuring SMTP:"
"The " { $vocab-link "smtp" } " vocabulary sends e-mail via an SMTP server."
$nl
"This library is configured by a set of dynamically-scoped variables:"
{ $subsection smtp-server }
{ $subsection smtp-read-timeout }
{ $subsection smtp-domain }
{ $subsection esmtp? }
{ $subsection smtp-auth }
"The latter is set to an instance of one of the following:"
{ $subsection no-auth }
{ $subsection plain-auth }
"Constructing an e-mail:"
{ $subsection email }
{ $subsection <email> }
"Sending an email:"
{ $subsection send-email } ;
ABOUT: "smtp"

View File

@ -1,8 +1,10 @@
USING: smtp tools.test io.streams.string io.sockets threads
smtp.server kernel sequences namespaces logging accessors
assocs sorting smtp.private ;
assocs sorting smtp.private concurrency.promises ;
IN: smtp.tests
\ send-email must-infer
{ 0 0 } [ [ ] with-smtp-connection ] must-infer-as
[ "hello\nworld" validate-address ] must-fail
@ -63,13 +65,13 @@ IN: smtp.tests
[ from>> extract-email ] tri
] unit-test
[ ] [ [ 4321 mock-smtp-server ] "SMTP server" spawn drop ] unit-test
<promise> "p" set
[ ] [ yield ] unit-test
[ ] [ "p" get mock-smtp-server ] unit-test
[ ] [
[
"localhost" 4321 <inet> smtp-server set
"localhost" "p" get ?promise <inet> smtp-server set
<email>
"Hi guys\nBye guys" >>body
@ -82,5 +84,3 @@ IN: smtp.tests
send-email
] with-scope
] unit-test
[ ] [ yield ] unit-test

View File

@ -1,16 +1,28 @@
! Copyright (C) 2007, 2008 Elie CHAFTARI, Dirk Vleugels,
! Slava Pestov, Doug Coleman.
! See http://factorcode.org/license.txt for BSD license.
USING: arrays namespaces make io io.timeouts kernel logging
io.sockets sequences combinators splitting assocs strings
math.parser random system calendar io.encodings.ascii summary
calendar.format accessors sets hashtables ;
USING: arrays namespaces make io io.encodings.string
io.encodings.utf8 io.timeouts kernel logging io.sockets
sequences combinators splitting assocs strings math.parser
random system calendar io.encodings.ascii summary
calendar.format accessors sets hashtables base64 ;
IN: smtp
SYMBOL: smtp-domain
SYMBOL: smtp-server "localhost" 25 <inet> smtp-server set-global
SYMBOL: smtp-read-timeout 1 minutes smtp-read-timeout set-global
SYMBOL: esmtp? t esmtp? set-global
SYMBOL: smtp-server
"localhost" 25 <inet> smtp-server set-global
SYMBOL: smtp-read-timeout
1 minutes smtp-read-timeout set-global
SINGLETON: no-auth
TUPLE: plain-auth username password ;
C: <plain-auth> plain-auth
SYMBOL: smtp-auth
no-auth smtp-auth set-global
LOG: log-smtp-connection NOTICE ( addrspec -- )
@ -34,12 +46,12 @@ TUPLE: email
: <email> ( -- email ) email new ;
<PRIVATE
: crlf ( -- ) "\r\n" write ;
: command ( string -- ) write crlf flush ;
: helo ( -- )
esmtp? get "EHLO " "HELO " ? host-name append command ;
: helo ( -- ) "EHLO " host-name append command ;
ERROR: bad-email-address email ;
@ -60,8 +72,7 @@ ERROR: bad-email-address email ;
ERROR: message-contains-dot message ;
M: message-contains-dot summary ( obj -- string )
drop
"Message cannot contain . on a line by itself" ;
drop "Message cannot contain . on a line by itself" ;
: validate-message ( msg -- msg' )
"." over member?
@ -115,7 +126,7 @@ ERROR: smtp-transaction-failed < smtp-error ;
3 swap ?nth CHAR: - = ;
: process-multiline ( multiline -- response )
>r readln r> 2dup " " append head? [
[ readln ] dip 2dup " " append head? [
drop dup smtp-response
] [
swap check-response process-multiline
@ -127,6 +138,16 @@ ERROR: smtp-transaction-failed < smtp-error ;
: get-ok ( -- ) receive-response check-response ;
GENERIC: send-auth ( auth -- )
M: no-auth send-auth drop ;
M: plain-auth send-auth
[ username>> ] [ password>> ] bi "\0" swap 3append utf8 encode >base64
"AUTH PLAIN " prepend command ;
: auth ( -- ) smtp-auth get send-auth get-ok ;
ERROR: invalid-header-string string ;
: validate-header ( string -- string' )
@ -170,6 +191,7 @@ ERROR: invalid-header-string string ;
: (send-email) ( headers email -- )
[
helo get-ok
auth
dup from>> extract-email mail-from get-ok
dup to>> [ extract-email rcpt-to get-ok ] each
dup cc>> [ extract-email rcpt-to get-ok ] each
@ -180,6 +202,7 @@ ERROR: invalid-header-string string ;
body>> send-body get-ok
quit get-ok
] with-smtp-connection ;
PRIVATE>
: send-email ( email -- )