Skip to content

Instantly share code, notes, and snippets.

@jeremyheiler
Last active December 14, 2015 03:38
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jeremyheiler/5022062 to your computer and use it in GitHub Desktop.
Save jeremyheiler/5022062 to your computer and use it in GitHub Desktop.
Possible ways to script SMTP with Clojure.
;; A data-oriented approach.
;; Basic SMTP. Falls back to HELO if Extended SMTP isn't supported.
(def script
[[:CONNECT ["server.example.com" 587]]
[:EHLO ["client.example.com"] :on-reply {502 [:HELO ["client.example.com"]]}]
[:MAIL ["foo@example.com"]]
[:RCPT ["bar@example.com"]]
[:RCPT ["baz@example.com"]]
[:DATA]
[:MESSAGE ["From: foo@example.com\r\nTo: bar@example.com, baz@example.com\r\nSubject: Test\r\n\r\nThis is a test!"]]
[:QUIT]])
;; Extended SMTP. Uses STARTTLS and AUTH extensions. (Good enough for Gmail.)
(def script-with-ext
[[:CONNECT ["server.example.com" 587]]
[:EHLO ["client.example.com"]]
[:STARTTLS]
[:EHLO ["client.example.com"]]
[:AUTH ["PLAIN" "dGVzdAB0ZXN0ADEyMzQ="]]
[:MAIL ["foo@example.com"]]
[:RCPT ["bar@example.com"]]
[:RCPT ["baz@example.com"]]
[:DATA]
[:MESSAGE ["From: foo@example.com\r\nTo: bar@example.com, baz@example.com\r\nSubject: Test\r\n\r\nThis is a test!"]]
[:QUIT]])
;; Note: The :MESSAGE key is not an SMTP command. It adds "\r\n.\r\n"to the end to complete the message.
;; The :CONNECT key is not an SMTP command. It esatblishs the connection and should handle a reply.
;; Another potential special command is :FREE for free-form writes.
;; A more "DSL-ish" approach.
;; Basic SMTP. Each function interacts with the server and handles a reply.
(script
(connect "server.example.com" 587)
(command :EHLO ["client.example.com"])
(command :MAIL ["foo@example.com"])
(command :RCPT ["bar@example.com"])
(command :RCPT ["baz@example.com"])
(command :DATA)
(message (format-message {:from "foo@example.com" :to ["bar@example.com" "baz@example.com"] :subject "Test" :body "This is a test!"}))
(command :QUIT))
;; The advantage here is that there are no pseudo commands.
;; Also, since it's not data, it's more difficult to manipulate.
;; However, that may not be as important for scripting SMTP.
;; The format-message function is not part of the DSL, but an ordinary function.
;; In fact, the message map at this point should look like one of the following:
(def message-map-a
{:headers ["From: foo@example.com"
"To: bar@example.com, baz@example.com"
"Subject: Test"]
:body "This is a test!"})
(def message-map-b
{:headers [{:name "From" :body "foo@example.com"}
{:name "To" :body "bar@example.com, baz@example.com"}
{:name "Subject" :body "Test"}]
:body "This is a test!"})
;; The interesting points are:
;; - The headers remain ordered.
;; - The headers are easy to parse by type/name.
;; - The headers are easy to wrap when formatting the message.
;; - That the message map can be nested for more complex message bodies.
;; The advantage of having a map represent a header is that additional meta data could be provided.
@jeremyheiler
Copy link
Author

@kisom
Copy link

kisom commented Feb 26, 2013

Looks very interesting. I particularly like the use of a DSL to make the SMTP conversation easier to follow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment