std/smtp
import std/smtp to use it. Source: src/std/smtp.iyi at v0.18.0.
import std/smtp::{SmtpClient, SmtpError, SmtpSecurity}
import std/mail::{Mail} smtp = SmtpClient.connect("mail.example.com", 587).or_panic # STARTTLS
smtp.auth("[email protected]", password).or_panic
smtp.send(msg).or_panic # a Mail::Message
smtp.quit**Connecting.** connect opens the connection, reads the server's 220, and says EHLO - HELO when the server does not know EHLO (a 500, 502 or 504), and then no extension is taken to be offered. With SmtpSecurity::StartTls, the default, the server must offer STARTTLS (RFC 3207): the client asks for it, makes the TLS handshake through std/tls, the certificate checked for the host against config, and says EHLO again, forgetting what the server said before (section 4.2). A server that does not offer it, or that sends anything after its 220 to STARTTLS - bytes that would be read as though they had come under TLS, the injection of CVE-2011-0411 - is refused. SmtpSecurity::Tls is TLS from the first byte, as port 465 speaks it (RFC 8314), and SmtpSecurity::Cleartext none. The name said with EHLO is the caller's, or the connection's own address as an address literal ([192.0.2.1], section 4.1.3), which needs no name lookup.
**Authenticating.** auth sends AUTH PLAIN (RFC 4616), LOGIN or CRAM-MD5 (RFC 2195), the first of these the server offers unless the caller names one. Over a connection without TLS it refuses to send the password unless cleartext_auth is set: a password in the clear is the one thing a mistaken configuration should not do quietly.
**Sending.** send takes a Mail::Message - its From as the sender, its To, Cc and Bcc as the recipients, its text with Bcc left out - or a sender, recipients and the message's bytes. MAIL FROM carries SIZE when the server offers it (RFC 1870), and a message larger than the size it names is refused before it is sent; BODY=8BITMIME (RFC 6152) when the message has bytes past ASCII, which a server that does not offer 8BITMIME is not sent; SMTPUTF8 (RFC 6531) when an address or the message is RFC 6532's UTF-8 and the server offers it, and the message is refused when it does not. Each RCPT TO must be taken - one that is not ends the transaction with RSET and is the error, so a message goes to all its recipients or to none. DATA sends the message with its line breaks made CRLF and each line that begins with a dot given another (section 4.5.2), then the lone dot.
**Replies** are read whole, continuation lines and all (section 4.2.1): lines whose codes disagree, a line that is not a reply, a line longer than 4096 bytes or a reply of more than 100 lines is refused, the connection closed. A reply that is not the one a command wants is an SmtpError whose code is its code and reply its text; an error below SMTP - the socket's, TLS's, a server gone - has code 0 and closes the connection. Every read and write waits at most timeout_ms (five minutes unless the caller says otherwise: section 4.5.3.2 asks for that much or more of the client at each step), and a wait that runs out is an error whose timed_out? is true.
Not here: PIPELINING, CHUNKING, DSN, and a password that std/tls has not kept from the wire because the caller set cleartext_auth. bench/std_smtp_exercise.sh holds it to a server written in Python for the purpose, over plain TCP, STARTTLS and TLS.
SmtpAuthenum
pub enum SmtpAuth : Int32
Plain = 0
Login = 1
CramMd5 = 2
endSmtpClientclass
A session with a mail server.
pub class SmtpClient
def self.ascii?(text : String) : Bool
# AUTH as *user* with *password*, by *mechanism*, or by the first the
# server offers of PLAIN, LOGIN and CRAM-MD5.
def auth(user : String, password : String, mechanism : SmtpAuth | ::Nil = nil) : SmtpError | ::Nil
# Whether a password may go over a connection without TLS.
def cleartext_auth : Bool
# Whether a password may go over a connection without TLS.
def cleartext_auth=(value : Bool) : Bool
# The connection closed, without QUIT.
def close : Nil
def closed? : Bool
# *line* sent, and its reply.
def command(line : String) : SmtpReply | SmtpError
# A session with *host* on *port*: connected, greeted, EHLO said, and
# under TLS as *security* asks.
def self.connect(host : String, port : Int32, security : SmtpSecurity = SmtpSecurity::StartTls, config : TlsConfig = TlsConfig.new, helo : String | ::Nil = nil, timeout_ms : Int32 = TIMEOUT_MS) : SmtpClient | SmtpError
def self.digit?(b : UInt8) : Bool
# What EHLO named, by its keyword in upper case: each extension's
# parameters ("" when none). Empty after HELO.
def extensions : Hash(String, String)
# The server's 220.
def greeting : SmtpReply
def initialize(socket : IyiSocket, host : String, helo : String, timeout_ms : Int32)
# `[192.0.2.1]`, or `[IPv6:2001:db8::1]` (section 4.1.3).
def self.literal(address : String) : String
def self.mechanism_name(mechanism : SmtpAuth) : String
protected def open(security : SmtpSecurity, config : TlsConfig) : SmtpError | ::Nil
# QUIT, its 221 read, and the connection closed.
def quit : SmtpError | ::Nil
# The next reply, every line of it.
def reply : SmtpReply | SmtpError
# *data*, a whole message, from *sender* to *recipients*; *utf8* when
# the message is RFC 6532's, as SMTPUTF8 carries.
def send(sender : String, recipients : Array(String), data : String, utf8 : Bool = false) : SmtpError | ::Nil
# *message* to its recipients - To, Cc and Bcc - from its From.
def send(message : Mail::Message) : SmtpError | ::Nil
# *data* as DATA sends it (section 4.5.2): line breaks CRLF, a dot
# doubled at a line's start, a CRLF at the end if it had none, and the
# lone dot after.
def self.stuffed(data : String) : String
# Whether the session is under TLS.
def tls? : Bool
def self.unsafe?(text : String) : Bool
endSmtpErrorstruct
What a session answered that it should not have, or could not do, as a value. code is the reply's code, 0 when there was no reply to have one; reply the reply's text, its lines joined with newlines.
pub struct SmtpError
def cancelled? : Bool
def code : Int32
def initialize(reason : String, code : Int32 = 0, reply : String = "", timed_out : Bool = false, cancelled : Bool = false)
def reason : String
def reply : String
# Whether a read or a write waited `timeout_ms` and nothing came.
def timed_out? : Bool
def to_s : String
endSmtpReplystruct
One reply: its code and its lines' text, the code and separator taken off each.
pub struct SmtpReply
def code : Int32
def initialize(code : Int32, lines : Array(String))
def lines : Array(String)
def text : String
endSmtpSecurityenum
How the connection is kept from the wire.
pub enum SmtpSecurity : Int32
StartTls = 0
Tls = 1
Cleartext = 2
endError for SmtpErrorimpl
impl Error for SmtpError
def message : String
end
An SMTP client (RFC 5321) over std/socket and std/tls, written in iyi: it hands a mail server what std/mail builds.