RFC-driven SMTP client and MIME message builder for MoonBit.
Dependencies
import "howhere7/moonmail"
async fn main {
let builder = @moonmail.MessageBuilder::new()
let _ = builder
.from("alice@example.com")
.to("bob@example.com")
.subject("Hello")
.text("This is a plain text message.")
let msg = builder.build()
let env = builder.envelope()
// Deterministic offline session using the scripted server.
let script = [
@moonmail.FakeStep::greeting(["220 smtp.example.com ESMTP"]),
@moonmail.FakeStep::reply("EHLO localhost", ["250 OK"]),
@moonmail.FakeStep::reply("MAIL FROM:<alice@example.com>", ["250 2.1.0 OK"]),
@moonmail.FakeStep::reply("RCPT TO:<bob@example.com>", ["250 2.1.5 OK"]),
@moonmail.FakeStep::reply("DATA", ["354 End data"]),
@moonmail.FakeStep::reply(".", ["250 2.0.0 OK"]),
@moonmail.FakeStep::reply("QUIT", ["221 2.0.0 Bye"]),
]
let client = @moonmail.SmtpClient::new(
@moonmail.FakeTransport::new(script),
@moonmail.SmtpConfig::new("localhost"),
)
let trace = client.send(msg, env, @moonmail.RenderConfig::deterministic())
println(trace.render())
}///|
let config = @moonmail.SmtpConfig::new("smtp.example.com", port=587).with_auth(
@moonmail.AuthMethod::Plain("user", "password"),
)
///|
let client = @moonmail.SmtpClient::new(@moonmail.SocketTransport::new(), config)
///|
let trace = client.send(msg, env, @moonmail.RenderConfig::new())moonmail send --smtp smtp.example.com --port 587 \
--from user@example.com --to a@b.com --cc c@d.com \
--subject "title" --body text.txt [--html] \
[--attach f1] [--attach f2] \
[--auth-user u --auth-pass p]
moonmail explain scenario.json # replay a FakeTransport script offlinemoon run cmd/main --target native -- send --help
moon run cmd/main --target native -- explain testdata/scenarios/basic.json| example | demonstrates |
|---|---|
| send_plain | plain text message |
| send_html | HTML body in multipart/alternative |
| send_attachment | binary attachment in multipart/mixed |
| send_multiple | To/Cc/Bcc multi-recipient envelope |
| fake_session | full scripted session over FakeTransport (offline) |
moon run examples/fake_session --target nativemoon check --deny-warn
moon check --target native/js/wasm-gc --deny-warn
moon test --target nativepub(open) trait SmtpTransport {
async fn connect(Self, SmtpConfig) -> Unit raise MailFailure
async fn write_line(Self, String) -> Unit raise MailFailure
async fn read_line(Self) -> String raise MailFailure
fn close(Self) -> Unit
}impl Show for MailFailureimpl Show for ContentTransferEncodingimpl Show for Dispositionimpl SmtpTransport for FakeTransportimpl Show for MailAddresspub struct MessageBuilder {
from : MailAddress?
to : Array[MailAddress]
cc : Array[MailAddress]
bcc : Array[MailAddress]
reply_to : Array[MailAddress]
subject : String
text_part : String?
html_part : String?
attachments : Array[Attachment]
inline_images : Array[Attachment]
custom_headers : Array[(String, String)]
date : String
message_id : String
boundary_seed : String
}fn MessageBuilder::attach(self : MessageBuilder, data : Bytes, filename : String, content_type? : String) -> MessageBuilderfn MessageBuilder::header(self : MessageBuilder, name : String, value : String) -> MessageBuilder raise MailFailurefn MessageBuilder::inline_image(self : MessageBuilder, data : Bytes, filename : String, cid : String) -> MessageBuilderfn MessageBuilder::reply_to(self : MessageBuilder, list : String) -> MessageBuilder raise MailFailurefn MessageBuilder::subject(self : MessageBuilder, subject : String) -> MessageBuilder raise MailFailurepub(all) enum MimeBody {
Text(String)
Html(String)
Binary(Bytes, String)
MultiPart(Array[Part], MimeSubtype)
} derive(Eq, Debug)impl Show for MimeSubtypepub(all) enum ReasonCode {
MM_HEADER_001
MM_HEADER_002
MM_HEADER_003
MM_HEADER_004
MM_ADDR_001
MM_ADDR_002
MM_ADDR_003
MM_ADDR_004
MM_ADDR_005
MM_ADDR_006
MM_ADDR_007
MM_ADDR_008
MM_ENCOD_001
MM_ENCOD_002
MM_ENCOD_003
MM_ENCOD_004
MM_ENCOD_005
MM_MIME_001
MM_MIME_002
MM_MIME_003
MM_MIME_004
MM_MIME_005
MM_MIME_006
MM_CRLF_001
MM_DOT_001
MM_INJ_001
MM_INJ_002
MM_INJ_003
MM_SMTP_001
MM_SMTP_002
MM_SMTP_003
MM_SMTP_004
MM_SMTP_005
MM_SMTP_006
MM_SMTP_007
MM_AUTH_001
MM_AUTH_002
MM_TRANS_001
MM_TRANS_002
Other
} derive(Eq, Debug)impl Show for ReasonCodeimpl Show for ServerReplyasync fn[T : SmtpTransport] SmtpClient::send(self : SmtpClient[T], message : MailMessage, envelope : Envelope, render : RenderConfig) -> SmtpTrace raise MailFailureasync fn[T : SmtpTransport] SmtpClient::send_rendered(self : SmtpClient[T], wire : String, envelope : Envelope) -> SmtpTrace raise MailFailurepub struct SmtpConfig {
host : String
port : Int
auth : AuthMethod?
timeout_ms : Int64
connect_retries : Int
} derive(Eq, Debug)pub struct SmtpSession[T] {
transport : T
config : SmtpConfig
state : SmtpState
caps : SmtpCaps
trace : SmtpTrace
}async fn[T : SmtpTransport] SmtpSession::auth(self : SmtpSession[T], auth : AuthMethod) -> Unit raise MailFailureasync fn[T : SmtpTransport] SmtpSession::mail_from(self : SmtpSession[T], reverse_path : String) -> Unit raise MailFailureasync fn[T : SmtpTransport] SmtpSession::rcpt_to(self : SmtpSession[T], forward_path : String) -> Unit raise MailFailureasync fn[T : SmtpTransport] SmtpSession::send_data(self : SmtpSession[T], lines : Array[String]) -> Unit raise MailFailurefn SmtpTrace::add_step(self : SmtpTrace, command : String, reply : ServerReply, duration_ms : Int64) -> Unitimpl SmtpTransport for SocketTransportlet CRAM_MD5_VECTOR_CHALLENGE : String--boundary\r\n
<child 1 rendered, ending with CRLF>
--boundary\r\n
<child 2 rendered>
--boundary--\r\nfn auth_plain_credentials(username : String, password : String) -> Stringfn base64_encode_str(s : String) -> Stringfn base64_mime_encode(data : Bytes) -> Stringfn boundary_collides(boundary : String, body : String) -> Boolfn bytes_to_string(b : Bytes) -> Stringfn contains_crlf(s : String) -> Boolfn cram_md5_response(username : String, password : String, challenge_b64 : String) -> String raise MailFailurefn display_name_to_string(display_name : String) -> Stringfn dot_stuff(s : String) -> Stringfn encode_body(content : String, encoding : ContentTransferEncoding) -> (String, ContentTransferEncoding) raise MailFailurefn encode_ehlo(host : String) -> Stringfn fixedarray_to_string(fa : FixedArray[Byte], len : Int) -> Stringfn generate_boundary(seed : String) -> Stringfn index_of(s : String, sub : String) -> Intfn is_pure_ascii(s : String) -> Boolfn is_valid_boundary(boundary : String) -> Boolfn is_valid_field_name(name : String) -> Boolfn normalize_crlf(s : String) -> Stringfn qp_encode(input : String) -> Stringfn redact_command(command : String) -> Stringfn sanitize_filename(name : String) -> Stringfn string_to_bytes(s : String) -> Bytesfn strip_reply_code(line : String) -> Stringfn unfold_header_value(value : String) -> Stringfn unique_boundary(seed : String, body : String) -> StringRFC-driven SMTP client and MIME message builder for MoonBit.
Dependencies