Why it exists
On the JVM this was solved twenty years ago. On Kotlin/Native there is nothing, and the two workarounds fail the same way: shelling out to sendmail and wrapping libcurl both hide the protocol exactly where it has to be visible.
Three decisions worth knowing about
Replies are matched by counting. The specification forbids matching by code or text, and it is right to: codes repeat and text is server-defined. Only order holds. With PIPELINING negotiated and five commands in flight, a matcher that reads codes pairs the wrong reply with the wrong command exactly when the session is busiest — silently.
A partial refusal is a result, not an exception. Three recipients accepted, two rejected: if that throws, the caller has lost which three worked, and the only safe options left are to give up or to retry all five and deliver three duplicates.
Anything a rule forbids is refused, not sanitised. A line break inside an address is a way to append headers of somebody else's choosing. Refusing means the caller finds out where the mistake was made.
What you get
- Submission per RFC 6409: port 587 with
STARTTLS, port 465 with implicit TLS. - TLS with the certificate chain and the host name verified — OpenSSL on Kotlin/Native,
SSLEngineon the JVM. - Seven SASL mechanisms:
PLAIN,LOGIN,CRAM-MD5,SCRAM-SHA-1,SCRAM-SHA-256,XOAUTH2,OAUTHBEARER. - ESMTP:
PIPELINING,SIZE,8BITMIME,SMTPUTF8,DSN,ENHANCEDSTATUSCODES,CHUNKING, plus punycode for internationalised domains. - Message building: RFC 5322 headers,
multipart/alternative, attachments, encoded words.
Install
repositories { maven("https://reposilite.kotlin.website/snapshots") }
dependencies { implementation("io.github.youndie:smtp-client:<VERSION>") }
linuxX64 is the platform this is built for and the only one it is claimed to work on — that is where the whole suite runs, including TLS against a real server and end to end against Mailpit and Postfix.