net.smtp: add STARTTLS and implicit SSL support (#13473)
parent
d4fc8601e0
commit
6d2a88e31f
|
@ -157,6 +157,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
|
|||
$if windows {
|
||||
skip_files << 'examples/database/mysql.v'
|
||||
skip_files << 'examples/database/orm.v'
|
||||
skip_files << 'examples/smtp/mail.v' // requires OpenSSL
|
||||
skip_files << 'examples/websocket/ping.v' // requires OpenSSL
|
||||
skip_files << 'examples/websocket/client-server/client.v' // requires OpenSSL
|
||||
skip_files << 'examples/websocket/client-server/server.v' // requires OpenSSL
|
||||
|
|
|
@ -53,6 +53,7 @@ const (
|
|||
'vlib/vweb/route_test.v',
|
||||
'vlib/net/websocket/websocket_test.v',
|
||||
'vlib/crypto/rand/crypto_rand_read_test.v',
|
||||
'vlib/net/smtp/smtp_test.v',
|
||||
]
|
||||
skip_with_fsanitize_address = [
|
||||
'vlib/net/websocket/websocket_test.v',
|
||||
|
@ -96,6 +97,7 @@ const (
|
|||
'vlib/net/http/server_test.v',
|
||||
'vlib/net/http/response_test.v',
|
||||
'vlib/builtin/js/array_test.js.v',
|
||||
'vlib/net/smtp/smtp_test.v',
|
||||
]
|
||||
skip_on_linux = [
|
||||
'do_not_remove',
|
||||
|
@ -121,6 +123,7 @@ const (
|
|||
'vlib/vweb/route_test.v',
|
||||
'vlib/sync/many_times_test.v',
|
||||
'vlib/sync/once_test.v',
|
||||
'vlib/net/smtp/smtp_test.v',
|
||||
]
|
||||
skip_on_non_windows = [
|
||||
'do_not_remove',
|
||||
|
|
|
@ -6,6 +6,7 @@ module smtp
|
|||
* Created by: nedimf (07/2020)
|
||||
*/
|
||||
import net
|
||||
import net.openssl
|
||||
import encoding.base64
|
||||
import strings
|
||||
import time
|
||||
|
@ -30,16 +31,20 @@ pub enum BodyType {
|
|||
|
||||
pub struct Client {
|
||||
mut:
|
||||
conn net.TcpConn
|
||||
reader io.BufferedReader
|
||||
conn net.TcpConn
|
||||
ssl_conn &openssl.SSLConn = 0
|
||||
reader io.BufferedReader
|
||||
pub:
|
||||
server string
|
||||
port int = 25
|
||||
username string
|
||||
password string
|
||||
from string
|
||||
ssl bool
|
||||
starttls bool
|
||||
pub mut:
|
||||
is_open bool
|
||||
is_open bool
|
||||
encrypted bool
|
||||
}
|
||||
|
||||
pub struct Mail {
|
||||
|
@ -55,6 +60,10 @@ pub struct Mail {
|
|||
|
||||
// new_client returns a new SMTP client and connects to it
|
||||
pub fn new_client(config Client) ?&Client {
|
||||
if config.ssl && config.starttls {
|
||||
return error('Can not use both implicit SSL and STARTTLS')
|
||||
}
|
||||
|
||||
mut c := &Client{
|
||||
...config
|
||||
}
|
||||
|
@ -71,10 +80,19 @@ pub fn (mut c Client) reconnect() ? {
|
|||
conn := net.dial_tcp('$c.server:$c.port') or { return error('Connecting to server failed') }
|
||||
c.conn = conn
|
||||
|
||||
c.reader = io.new_buffered_reader(reader: c.conn)
|
||||
if c.ssl {
|
||||
c.connect_ssl() ?
|
||||
} else {
|
||||
c.reader = io.new_buffered_reader(reader: c.conn)
|
||||
}
|
||||
|
||||
c.expect_reply(.ready) or { return error('Received invalid response from server') }
|
||||
c.send_ehlo() or { return error('Sending EHLO packet failed') }
|
||||
|
||||
if c.starttls && !c.encrypted {
|
||||
c.send_starttls() or { return error('Sending STARTTLS failed') }
|
||||
}
|
||||
|
||||
c.send_auth() or { return error('Authenticating to server failed') }
|
||||
c.is_open = true
|
||||
}
|
||||
|
@ -98,15 +116,41 @@ pub fn (mut c Client) send(config Mail) ? {
|
|||
pub fn (mut c Client) quit() ? {
|
||||
c.send_str('QUIT\r\n') ?
|
||||
c.expect_reply(.close) ?
|
||||
c.conn.close() ?
|
||||
if c.encrypted {
|
||||
c.ssl_conn.shutdown() ?
|
||||
} else {
|
||||
c.conn.close() ?
|
||||
}
|
||||
c.is_open = false
|
||||
c.encrypted = false
|
||||
}
|
||||
|
||||
fn (mut c Client) connect_ssl() ? {
|
||||
c.ssl_conn = openssl.new_ssl_conn()
|
||||
c.ssl_conn.connect(mut c.conn, c.server) or {
|
||||
return error('Connecting to server using OpenSSL failed: $err')
|
||||
}
|
||||
|
||||
c.reader = io.new_buffered_reader(reader: c.ssl_conn)
|
||||
c.encrypted = true
|
||||
}
|
||||
|
||||
// expect_reply checks if the SMTP server replied with the expected reply code
|
||||
fn (mut c Client) expect_reply(expected ReplyCode) ? {
|
||||
bytes := io.read_all(reader: c.conn) ?
|
||||
mut str := ''
|
||||
for {
|
||||
str = c.reader.read_line() ?
|
||||
if str.len < 4 {
|
||||
return error('Invalid SMTP response: $str')
|
||||
}
|
||||
|
||||
if str.runes()[3] == `-` {
|
||||
continue
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
str := bytes.bytestr().trim_space()
|
||||
$if smtp_debug ? {
|
||||
eprintln('\n\n[RECV]')
|
||||
eprint(str)
|
||||
|
@ -129,7 +173,12 @@ fn (mut c Client) send_str(s string) ? {
|
|||
eprint(s.trim_space())
|
||||
eprintln('\n[SEND END]')
|
||||
}
|
||||
c.conn.write(s.bytes()) ?
|
||||
|
||||
if c.encrypted {
|
||||
c.ssl_conn.write(s.bytes()) ?
|
||||
} else {
|
||||
c.conn.write(s.bytes()) ?
|
||||
}
|
||||
}
|
||||
|
||||
[inline]
|
||||
|
@ -138,6 +187,13 @@ fn (mut c Client) send_ehlo() ? {
|
|||
c.expect_reply(.action_ok) ?
|
||||
}
|
||||
|
||||
[inline]
|
||||
fn (mut c Client) send_starttls() ? {
|
||||
c.send_str('STARTTLS\r\n') ?
|
||||
c.expect_reply(.ready) ?
|
||||
c.connect_ssl() ?
|
||||
}
|
||||
|
||||
[inline]
|
||||
fn (mut c Client) send_auth() ? {
|
||||
if c.username.len == 0 {
|
||||
|
|
|
@ -8,21 +8,14 @@ fn fn_errors(mut c smtp.Client, m smtp.Mail) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* smtp_test
|
||||
* Created by: nedimf (07/2020)
|
||||
*/
|
||||
fn test_smtp() {
|
||||
$if !network ? {
|
||||
return
|
||||
}
|
||||
|
||||
fn send_mail(starttls bool) {
|
||||
client_cfg := smtp.Client{
|
||||
server: 'smtp.mailtrap.io'
|
||||
port: 465
|
||||
from: 'dev@vlang.io'
|
||||
username: os.getenv('VSMTP_TEST_USER')
|
||||
password: os.getenv('VSMTP_TEST_PASS')
|
||||
starttls: starttls
|
||||
}
|
||||
if client_cfg.username == '' && client_cfg.password == '' {
|
||||
eprintln('Please set VSMTP_TEST_USER and VSMTP_TEST_PASS before running this test')
|
||||
|
@ -87,3 +80,46 @@ fn test_smtp() {
|
|||
}
|
||||
assert true
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
* smtp_test
|
||||
* Created by: nedimf (07/2020)
|
||||
*/
|
||||
fn test_smtp() {
|
||||
$if !network ? {
|
||||
return
|
||||
}
|
||||
|
||||
// Test sending without STARTTLS
|
||||
send_mail(false)
|
||||
|
||||
// Sleep for 10 seconds to reset the Mailtrap rate limit counter
|
||||
// See: https://help.mailtrap.io/article/44-features-and-limits#rate-limit
|
||||
time.sleep(10000 * time.millisecond)
|
||||
|
||||
// Test with STARTTLS
|
||||
send_mail(true)
|
||||
}
|
||||
|
||||
fn test_smtp_implicit_ssl() {
|
||||
$if !network ? {
|
||||
return
|
||||
}
|
||||
|
||||
client_cfg := smtp.Client{
|
||||
server: 'smtp.gmail.com'
|
||||
port: 465
|
||||
from: ''
|
||||
username: ''
|
||||
password: ''
|
||||
ssl: true
|
||||
}
|
||||
|
||||
mut client := smtp.new_client(client_cfg) or {
|
||||
assert false
|
||||
return
|
||||
}
|
||||
|
||||
assert client.is_open && client.encrypted
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue