Skip to content

Bind net/smtp: Net::SMTP over the real socket/TLS transport - #205

Closed
tannevaled wants to merge 1 commit into
mainfrom
feat/bind-net-smtp
Closed

tannevaled wants to merge 1 commit into
mainfrom
feat/bind-net-smtp

Conversation

@tannevaled

Copy link
Copy Markdown
Contributor

Wires github.com/go-ruby-net-smtp/net-smtp — the MRI-byte-exact SMTP codec (command grammar, DATA dot-stuffing, reply parser, EHLO capability parse, SASL PLAIN/LOGIN/CRAM-MD5) — into rbgo so require "net/smtp" provides a native Net::SMTP with no .rb file.

Transport

The codec deliberately leaves out the socket, so — unlike the redis/pg bindings' injected-IO seam — the transport here is rbgo's real socket layer (socket.go): Net::SMTP#start dials a TCPSocket, wraps it in a smtpConn satisfying the library's Conn seam (WriteLine/ReadLine/WriteRaw/StartTLS), and drives netsmtp.Session against it exactly the way MRI drives its @socket. STARTTLS upgrades the same socket in place through the crypto/tls handshake the net/http transport already uses; implicit TLS (SMTPS, tls: true) wraps on connect. TLS defaults to VERIFY_NONE, matching the transport default (full cert-store verification is a follow-up).

Ruby surface bound

  • Construction: Net::SMTP.new(addr[, port]), Net::SMTP.start(...) (block + non-block; positional and keyword credentials; tls: / starttls:), default_port / default_tls_port / default_ssl_port / default_submission_port
  • Lifecycle: start / finish / started?
  • Sending: send_message / sendmail, open_message_stream / ready (yields a puts/print/write/<< adapter), data (string or block)
  • Low-level: ehlo / helo / mailfrom / rcptto / rcptto_list / rset, authenticate (:plain / :login / :cram_md5)
  • Config & capabilities: esmtp? / esmtp=, enable_tls / enable_ssl / disable_tls, enable_starttls / enable_starttls_auto / disable_starttls, tls? / ssl? / starttls? / starttls_auto? / starttls_always? (exclusivity enforced), open_timeout / read_timeout, capable_starttls? / capable_plain_auth? / capable_login_auth? / capable_cram_md5_auth? / capable_auth_types
  • A Net::SMTP::Response view (status / string / message / success? / continue?)
  • The Net::SMTPError family (SMTPError < ProtocolError; ServerBusy / Syntax / Authentication / Fatal / Unknown / UnsupportedCommand), so a codec error rescues as its MRI-faithful class

ActionMailer's SMTPDelivery could now be backed by this — left as a follow-up (not wired here).

Wiring

registerNetSMTP() in the builtins bootstrap (after registerSocket/registerNetHTTP), providedFeatures["net/smtp"], classOf cases for the three new value types, mirroring the redis/pg/net-http split.

Tests

Tests drive an in-process scripted SMTP server (plaintext, STARTTLS-upgrade, and implicit-TLS variants using a generated self-signed cert) plus Go-level unit tests for the connless / nil-session branches — deterministic, no real network, goroutines joined on cleanup.

  • 100% coverage of net_smtp.go + net_smtp_bind.go
  • Full internal/vm + internal/object suites green (76.8s / 0.3s)
  • gofmt -l empty, go vet clean, -race clean on the new tests
  • Builds on all six 64-bit arches (amd64/arm64/riscv64/loong64/ppc64le/s390x)

CGO=0, GOWORK=off, go 1.26.4, BSD-3-Clause.

🤖 Generated with Claude Code

Wire github.com/go-ruby-net-smtp/net-smtp (the MRI-byte-exact SMTP codec:
command grammar, DATA dot-stuffing, reply parser, EHLO capability parse,
SASL PLAIN/LOGIN/CRAM-MD5) into rbgo so `require "net/smtp"` provides a
native Net::SMTP with no .rb file.

The codec deliberately leaves out the socket, so — unlike the redis/pg
bindings' injected-IO seam — the transport here is rbgo's real socket layer
(socket.go): Net::SMTP#start dials a TCPSocket, wraps it in a smtpConn
satisfying the library's Conn seam, and drives netsmtp.Session against it.
STARTTLS upgrades the same socket in place through the crypto/tls handshake
the net/http transport already uses; implicit TLS (SMTPS) wraps on connect.

Ruby surface: Net::SMTP.new / .start (block + non-block, positional and
keyword credentials, tls:/starttls:), start/finish, send_message / sendmail,
open_message_stream / ready (with a puts/print/write/<< adapter), data
(string or block), the low-level ehlo/helo/mailfrom/rcptto/rcptto_list/rset,
authenticate (:plain/:login/:cram_md5), the esmtp/tls/starttls config and
capability predicates, timeouts, and a Net::SMTP::Response view. The
Net::SMTPError family (SMTPError < ProtocolError; ServerBusy/Syntax/
Authentication/Fatal/Unknown/UnsupportedCommand) is registered so a codec
error rescues as its MRI-faithful class.

Tests drive an in-process scripted SMTP server (plaintext, STARTTLS-upgrade
and implicit-TLS variants with a generated self-signed cert) plus Go-level
unit tests for the connless/nil-session branches — deterministic, no real
network, goroutines joined on cleanup. 100% coverage of the new files; full
internal/vm + internal/object suites green; gofmt/vet/-race clean; builds on
all six 64-bit arches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@tannevaled

Copy link
Copy Markdown
Contributor Author

Superseded by the net-wave consolidation PR (single merge onto main; shared builtins/require/object_model). Work preserved verbatim; branch kept.

@tannevaled tannevaled closed this Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant