iyi

Standard library, 0.18.0

std/smtp

An SMTP client (RFC 5321) over std/socket and std/tls, written in iyi: it hands a mail server what std/mail builds.

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

std/smtpiyi doc src/std/smtp.iyi
pub enum SmtpAuth : Int32
  Plain = 0
  Login = 1
  CramMd5 = 2
end

SmtpClientclass

A session with a mail server.

std/smtpiyi doc src/std/smtp.iyi
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
end

SmtpErrorstruct

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.

std/smtpiyi doc src/std/smtp.iyi
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
end

SmtpReplystruct

One reply: its code and its lines' text, the code and separator taken off each.

std/smtpiyi doc src/std/smtp.iyi
pub struct SmtpReply
  def code : Int32
  def initialize(code : Int32, lines : Array(String))
  def lines : Array(String)
  def text : String
end

SmtpSecurityenum

How the connection is kept from the wire.

std/smtpiyi doc src/std/smtp.iyi
pub enum SmtpSecurity : Int32
  StartTls = 0
  Tls = 1
  Cleartext = 2
end

Error for SmtpErrorimpl

std/smtpiyi doc src/std/smtp.iyi
impl Error for SmtpError
  def message : String
end