2020-07-11 22:05:24 +03:00
|
|
|
module smtp
|
|
|
|
|
|
|
|
/*
|
|
|
|
*
|
2020-07-13 17:41:23 +03:00
|
|
|
* smtp module
|
2020-07-11 22:05:24 +03:00
|
|
|
* Created by: nedimf (07/2020)
|
|
|
|
*/
|
|
|
|
import net
|
2022-09-22 16:50:34 +03:00
|
|
|
import net.ssl
|
2020-07-11 22:05:24 +03:00
|
|
|
import encoding.base64
|
|
|
|
import strings
|
2020-07-13 17:41:23 +03:00
|
|
|
import time
|
2020-11-15 23:54:47 +03:00
|
|
|
import io
|
2020-07-11 22:05:24 +03:00
|
|
|
|
2020-07-13 17:41:23 +03:00
|
|
|
const (
|
2021-01-20 13:11:01 +03:00
|
|
|
recv_size = 128
|
2020-07-13 17:41:23 +03:00
|
|
|
)
|
|
|
|
|
|
|
|
enum ReplyCode {
|
2021-01-20 13:11:01 +03:00
|
|
|
ready = 220
|
|
|
|
close = 221
|
|
|
|
auth_ok = 235
|
|
|
|
action_ok = 250
|
2020-07-13 17:41:23 +03:00
|
|
|
mail_start = 354
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|
|
|
|
|
2020-07-13 17:41:23 +03:00
|
|
|
pub enum BodyType {
|
|
|
|
text
|
|
|
|
html
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|
|
|
|
|
2020-07-13 17:41:23 +03:00
|
|
|
pub struct Client {
|
|
|
|
mut:
|
2022-02-16 10:18:51 +03:00
|
|
|
conn net.TcpConn
|
2022-09-22 16:50:34 +03:00
|
|
|
ssl_conn &ssl.SSLConn = unsafe { nil }
|
2023-05-02 22:54:57 +03:00
|
|
|
reader ?&io.BufferedReader
|
2020-07-13 17:41:23 +03:00
|
|
|
pub:
|
|
|
|
server string
|
|
|
|
port int = 25
|
|
|
|
username string
|
|
|
|
password string
|
|
|
|
from string
|
2022-02-16 10:18:51 +03:00
|
|
|
ssl bool
|
|
|
|
starttls bool
|
2020-07-13 17:41:23 +03:00
|
|
|
pub mut:
|
2022-02-16 10:18:51 +03:00
|
|
|
is_open bool
|
|
|
|
encrypted bool
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|
|
|
|
|
2020-07-13 17:41:23 +03:00
|
|
|
pub struct Mail {
|
|
|
|
from string
|
|
|
|
to string
|
|
|
|
cc string
|
|
|
|
bcc string
|
|
|
|
date time.Time = time.now()
|
|
|
|
subject string
|
|
|
|
body_type BodyType
|
|
|
|
body string
|
|
|
|
}
|
|
|
|
|
|
|
|
// new_client returns a new SMTP client and connects to it
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn new_client(config Client) !&Client {
|
2022-02-16 10:18:51 +03:00
|
|
|
if config.ssl && config.starttls {
|
|
|
|
return error('Can not use both implicit SSL and STARTTLS')
|
|
|
|
}
|
|
|
|
|
2021-01-20 13:11:01 +03:00
|
|
|
mut c := &Client{
|
|
|
|
...config
|
|
|
|
}
|
2022-10-16 09:28:57 +03:00
|
|
|
c.reconnect()!
|
2020-07-13 17:41:23 +03:00
|
|
|
return c
|
|
|
|
}
|
|
|
|
|
|
|
|
// reconnect reconnects to the SMTP server if the connection was closed
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn (mut c Client) reconnect() ! {
|
2021-01-20 13:11:01 +03:00
|
|
|
if c.is_open {
|
|
|
|
return error('Already connected to server')
|
|
|
|
}
|
2020-07-13 17:41:23 +03:00
|
|
|
|
2022-11-15 16:53:13 +03:00
|
|
|
conn := net.dial_tcp('${c.server}:${c.port}') or { return error('Connecting to server failed') }
|
2020-11-15 23:54:47 +03:00
|
|
|
c.conn = conn
|
|
|
|
|
2022-02-16 10:18:51 +03:00
|
|
|
if c.ssl {
|
2022-10-16 09:28:57 +03:00
|
|
|
c.connect_ssl()!
|
2022-02-16 10:18:51 +03:00
|
|
|
} else {
|
|
|
|
c.reader = io.new_buffered_reader(reader: c.conn)
|
|
|
|
}
|
2020-07-13 17:41:23 +03:00
|
|
|
|
|
|
|
c.expect_reply(.ready) or { return error('Received invalid response from server') }
|
|
|
|
c.send_ehlo() or { return error('Sending EHLO packet failed') }
|
2022-02-16 10:18:51 +03:00
|
|
|
|
|
|
|
if c.starttls && !c.encrypted {
|
|
|
|
c.send_starttls() or { return error('Sending STARTTLS failed') }
|
|
|
|
}
|
|
|
|
|
2020-07-13 17:41:23 +03:00
|
|
|
c.send_auth() or { return error('Authenticating to server failed') }
|
|
|
|
c.is_open = true
|
|
|
|
}
|
|
|
|
|
|
|
|
// send sends an email
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn (mut c Client) send(config Mail) ! {
|
2021-01-20 13:11:01 +03:00
|
|
|
if !c.is_open {
|
|
|
|
return error('Disconnected from server')
|
|
|
|
}
|
2020-07-13 17:41:23 +03:00
|
|
|
from := if config.from != '' { config.from } else { c.from }
|
|
|
|
c.send_mailfrom(from) or { return error('Sending mailfrom failed') }
|
|
|
|
c.send_mailto(config.to) or { return error('Sending mailto failed') }
|
|
|
|
c.send_data() or { return error('Sending mail data failed') }
|
2021-01-23 01:24:48 +03:00
|
|
|
c.send_body(Mail{
|
|
|
|
...config
|
2021-01-20 13:11:01 +03:00
|
|
|
from: from
|
|
|
|
}) or { return error('Sending mail body failed') }
|
2020-07-13 17:41:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// quit closes the connection to the server
|
2022-10-16 09:28:57 +03:00
|
|
|
pub fn (mut c Client) quit() ! {
|
|
|
|
c.send_str('QUIT\r\n')!
|
|
|
|
c.expect_reply(.close)!
|
2022-02-16 10:18:51 +03:00
|
|
|
if c.encrypted {
|
2022-10-16 09:28:57 +03:00
|
|
|
c.ssl_conn.shutdown()!
|
2022-02-16 10:18:51 +03:00
|
|
|
} else {
|
2022-10-16 09:28:57 +03:00
|
|
|
c.conn.close()!
|
2022-02-16 10:18:51 +03:00
|
|
|
}
|
2020-07-13 17:41:23 +03:00
|
|
|
c.is_open = false
|
2022-02-16 10:18:51 +03:00
|
|
|
c.encrypted = false
|
|
|
|
}
|
|
|
|
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut c Client) connect_ssl() ! {
|
|
|
|
c.ssl_conn = ssl.new_ssl_conn()!
|
2022-02-16 10:18:51 +03:00
|
|
|
c.ssl_conn.connect(mut c.conn, c.server) or {
|
2022-11-15 16:53:13 +03:00
|
|
|
return error('Connecting to server using OpenSSL failed: ${err}')
|
2022-02-16 10:18:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
c.reader = io.new_buffered_reader(reader: c.ssl_conn)
|
|
|
|
c.encrypted = true
|
2020-07-13 17:41:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// expect_reply checks if the SMTP server replied with the expected reply code
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut c Client) expect_reply(expected ReplyCode) ! {
|
2022-02-16 10:18:51 +03:00
|
|
|
mut str := ''
|
|
|
|
for {
|
2023-05-02 22:54:57 +03:00
|
|
|
str = c.reader or { return error('the Client.reader field is not set') }.read_line()!
|
2022-02-16 10:18:51 +03:00
|
|
|
if str.len < 4 {
|
2022-11-15 16:53:13 +03:00
|
|
|
return error('Invalid SMTP response: ${str}')
|
2022-02-16 10:18:51 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
if str.runes()[3] == `-` {
|
|
|
|
continue
|
|
|
|
} else {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2020-07-13 17:41:23 +03:00
|
|
|
|
2021-01-20 13:11:01 +03:00
|
|
|
$if smtp_debug ? {
|
2020-11-15 23:54:47 +03:00
|
|
|
eprintln('\n\n[RECV]')
|
2020-07-13 17:41:23 +03:00
|
|
|
eprint(str)
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|
2020-07-13 17:41:23 +03:00
|
|
|
|
2020-11-15 23:54:47 +03:00
|
|
|
if str.len >= 3 {
|
2020-07-13 17:41:23 +03:00
|
|
|
status := str[..3].int()
|
2022-10-02 22:39:11 +03:00
|
|
|
if unsafe { ReplyCode(status) } != expected {
|
2022-11-15 16:53:13 +03:00
|
|
|
return error('Received unexpected status code ${status}, expecting ${expected}')
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|
2021-01-20 13:11:01 +03:00
|
|
|
} else {
|
2022-11-15 16:53:13 +03:00
|
|
|
return error('Recieved unexpected SMTP data: ${str}')
|
2021-01-20 13:11:01 +03:00
|
|
|
}
|
2020-07-13 17:41:23 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
[inline]
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut c Client) send_str(s string) ! {
|
2021-01-20 13:11:01 +03:00
|
|
|
$if smtp_debug ? {
|
2020-07-13 17:41:23 +03:00
|
|
|
eprintln('\n\n[SEND START]')
|
|
|
|
eprint(s.trim_space())
|
|
|
|
eprintln('\n[SEND END]')
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|
2022-02-16 10:18:51 +03:00
|
|
|
|
|
|
|
if c.encrypted {
|
2022-10-16 09:28:57 +03:00
|
|
|
c.ssl_conn.write(s.bytes())!
|
2022-02-16 10:18:51 +03:00
|
|
|
} else {
|
2022-10-16 09:28:57 +03:00
|
|
|
c.conn.write(s.bytes())!
|
2022-02-16 10:18:51 +03:00
|
|
|
}
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|
|
|
|
|
2020-07-13 17:41:23 +03:00
|
|
|
[inline]
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut c Client) send_ehlo() ! {
|
2022-11-15 16:53:13 +03:00
|
|
|
c.send_str('EHLO ${c.server}\r\n')!
|
2022-10-16 09:28:57 +03:00
|
|
|
c.expect_reply(.action_ok)!
|
2020-07-13 17:41:23 +03:00
|
|
|
}
|
|
|
|
|
2022-02-16 10:18:51 +03:00
|
|
|
[inline]
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut c Client) send_starttls() ! {
|
|
|
|
c.send_str('STARTTLS\r\n')!
|
|
|
|
c.expect_reply(.ready)!
|
|
|
|
c.connect_ssl()!
|
2022-02-16 10:18:51 +03:00
|
|
|
}
|
|
|
|
|
2020-07-13 17:41:23 +03:00
|
|
|
[inline]
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut c Client) send_auth() ! {
|
2020-07-13 18:22:36 +03:00
|
|
|
if c.username.len == 0 {
|
|
|
|
return
|
2021-01-20 13:11:01 +03:00
|
|
|
}
|
2020-07-11 22:05:24 +03:00
|
|
|
mut sb := strings.new_builder(100)
|
2022-04-15 14:58:56 +03:00
|
|
|
sb.write_u8(0)
|
2021-02-22 14:18:11 +03:00
|
|
|
sb.write_string(c.username)
|
2022-04-15 14:58:56 +03:00
|
|
|
sb.write_u8(0)
|
2021-02-22 14:18:11 +03:00
|
|
|
sb.write_string(c.password)
|
2020-07-13 17:41:23 +03:00
|
|
|
a := sb.str()
|
2021-02-26 09:22:12 +03:00
|
|
|
auth := 'AUTH PLAIN ${base64.encode_str(a)}\r\n'
|
2022-10-16 09:28:57 +03:00
|
|
|
c.send_str(auth)!
|
|
|
|
c.expect_reply(.auth_ok)!
|
2020-07-13 17:41:23 +03:00
|
|
|
}
|
|
|
|
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut c Client) send_mailfrom(from string) ! {
|
2022-11-15 16:53:13 +03:00
|
|
|
c.send_str('MAIL FROM: <${from}>\r\n')!
|
2022-10-16 09:28:57 +03:00
|
|
|
c.expect_reply(.action_ok)!
|
2020-07-13 17:41:23 +03:00
|
|
|
}
|
|
|
|
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut c Client) send_mailto(to string) ! {
|
2022-08-23 20:50:41 +03:00
|
|
|
for rcpt in to.split(';') {
|
2022-11-15 16:53:13 +03:00
|
|
|
c.send_str('RCPT TO: <${rcpt}>\r\n')!
|
2022-10-16 09:28:57 +03:00
|
|
|
c.expect_reply(.action_ok)!
|
2022-08-23 20:50:41 +03:00
|
|
|
}
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|
|
|
|
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut c Client) send_data() ! {
|
|
|
|
c.send_str('DATA\r\n')!
|
|
|
|
c.expect_reply(.mail_start)!
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|
|
|
|
|
2022-10-16 09:28:57 +03:00
|
|
|
fn (mut c Client) send_body(cfg Mail) ! {
|
2020-07-13 17:41:23 +03:00
|
|
|
is_html := cfg.body_type == .html
|
2022-05-08 09:15:45 +03:00
|
|
|
date := cfg.date.custom_format('ddd, D MMM YYYY HH:mm ZZ')
|
2022-05-16 11:09:36 +03:00
|
|
|
nonascii_subject := cfg.subject.bytes().any(it < u8(` `) || it > u8(`~`))
|
2020-07-13 17:41:23 +03:00
|
|
|
mut sb := strings.new_builder(200)
|
2022-11-15 16:53:13 +03:00
|
|
|
sb.write_string('From: ${cfg.from}\r\n')
|
2022-08-23 20:50:41 +03:00
|
|
|
sb.write_string('To: <${cfg.to.split(';').join('>; <')}>\r\n')
|
|
|
|
sb.write_string('Cc: <${cfg.cc.split(';').join('>; <')}>\r\n')
|
|
|
|
sb.write_string('Bcc: <${cfg.bcc.split(';').join('>; <')}>\r\n')
|
2022-11-15 16:53:13 +03:00
|
|
|
sb.write_string('Date: ${date}\r\n')
|
2022-05-16 11:09:36 +03:00
|
|
|
if nonascii_subject {
|
|
|
|
// handle UTF-8 subjects according RFC 1342
|
|
|
|
sb.write_string('Subject: =?utf-8?B?' + base64.encode_str(cfg.subject) + '?=\r\n')
|
|
|
|
} else {
|
2022-11-15 16:53:13 +03:00
|
|
|
sb.write_string('Subject: ${cfg.subject}\r\n')
|
2022-05-16 11:09:36 +03:00
|
|
|
}
|
|
|
|
|
2020-07-13 17:41:23 +03:00
|
|
|
if is_html {
|
2022-08-29 09:19:46 +03:00
|
|
|
sb.write_string('Content-Type: text/html; charset=UTF-8\r\n')
|
2022-05-08 09:15:45 +03:00
|
|
|
} else {
|
2022-08-29 09:19:46 +03:00
|
|
|
sb.write_string('Content-Type: text/plain; charset=UTF-8\r\n')
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|
2022-08-29 09:19:46 +03:00
|
|
|
sb.write_string('Content-Transfer-Encoding: base64')
|
2021-02-22 14:18:11 +03:00
|
|
|
sb.write_string('\r\n\r\n')
|
2022-08-29 09:19:46 +03:00
|
|
|
sb.write_string(base64.encode_str(cfg.body))
|
2021-02-22 14:18:11 +03:00
|
|
|
sb.write_string('\r\n.\r\n')
|
2022-10-16 09:28:57 +03:00
|
|
|
c.send_str(sb.str())!
|
|
|
|
c.expect_reply(.action_ok)!
|
2020-07-11 22:05:24 +03:00
|
|
|
}
|