net.smtp: add STARTTLS and implicit SSL support (#13473)

pull/13493/head
starryskye 2022-02-16 00:18:51 -07:00 committed by GitHub
parent d4fc8601e0
commit 6d2a88e31f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 114 additions and 18 deletions

View File

@ -157,6 +157,7 @@ pub fn new_test_session(_vargs string, will_compile bool) TestSession {
$if windows { $if windows {
skip_files << 'examples/database/mysql.v' skip_files << 'examples/database/mysql.v'
skip_files << 'examples/database/orm.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/ping.v' // requires OpenSSL
skip_files << 'examples/websocket/client-server/client.v' // requires OpenSSL skip_files << 'examples/websocket/client-server/client.v' // requires OpenSSL
skip_files << 'examples/websocket/client-server/server.v' // requires OpenSSL skip_files << 'examples/websocket/client-server/server.v' // requires OpenSSL

View File

@ -53,6 +53,7 @@ const (
'vlib/vweb/route_test.v', 'vlib/vweb/route_test.v',
'vlib/net/websocket/websocket_test.v', 'vlib/net/websocket/websocket_test.v',
'vlib/crypto/rand/crypto_rand_read_test.v', 'vlib/crypto/rand/crypto_rand_read_test.v',
'vlib/net/smtp/smtp_test.v',
] ]
skip_with_fsanitize_address = [ skip_with_fsanitize_address = [
'vlib/net/websocket/websocket_test.v', 'vlib/net/websocket/websocket_test.v',
@ -96,6 +97,7 @@ const (
'vlib/net/http/server_test.v', 'vlib/net/http/server_test.v',
'vlib/net/http/response_test.v', 'vlib/net/http/response_test.v',
'vlib/builtin/js/array_test.js.v', 'vlib/builtin/js/array_test.js.v',
'vlib/net/smtp/smtp_test.v',
] ]
skip_on_linux = [ skip_on_linux = [
'do_not_remove', 'do_not_remove',
@ -121,6 +123,7 @@ const (
'vlib/vweb/route_test.v', 'vlib/vweb/route_test.v',
'vlib/sync/many_times_test.v', 'vlib/sync/many_times_test.v',
'vlib/sync/once_test.v', 'vlib/sync/once_test.v',
'vlib/net/smtp/smtp_test.v',
] ]
skip_on_non_windows = [ skip_on_non_windows = [
'do_not_remove', 'do_not_remove',

View File

@ -6,6 +6,7 @@ module smtp
* Created by: nedimf (07/2020) * Created by: nedimf (07/2020)
*/ */
import net import net
import net.openssl
import encoding.base64 import encoding.base64
import strings import strings
import time import time
@ -30,16 +31,20 @@ pub enum BodyType {
pub struct Client { pub struct Client {
mut: mut:
conn net.TcpConn conn net.TcpConn
reader io.BufferedReader ssl_conn &openssl.SSLConn = 0
reader io.BufferedReader
pub: pub:
server string server string
port int = 25 port int = 25
username string username string
password string password string
from string from string
ssl bool
starttls bool
pub mut: pub mut:
is_open bool is_open bool
encrypted bool
} }
pub struct Mail { pub struct Mail {
@ -55,6 +60,10 @@ pub struct Mail {
// new_client returns a new SMTP client and connects to it // new_client returns a new SMTP client and connects to it
pub fn new_client(config Client) ?&Client { 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{ mut c := &Client{
...config ...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') } conn := net.dial_tcp('$c.server:$c.port') or { return error('Connecting to server failed') }
c.conn = conn 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.expect_reply(.ready) or { return error('Received invalid response from server') }
c.send_ehlo() or { return error('Sending EHLO packet failed') } 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.send_auth() or { return error('Authenticating to server failed') }
c.is_open = true c.is_open = true
} }
@ -98,15 +116,41 @@ pub fn (mut c Client) send(config Mail) ? {
pub fn (mut c Client) quit() ? { pub fn (mut c Client) quit() ? {
c.send_str('QUIT\r\n') ? c.send_str('QUIT\r\n') ?
c.expect_reply(.close) ? c.expect_reply(.close) ?
c.conn.close() ? if c.encrypted {
c.ssl_conn.shutdown() ?
} else {
c.conn.close() ?
}
c.is_open = false 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 // expect_reply checks if the SMTP server replied with the expected reply code
fn (mut c Client) expect_reply(expected ReplyCode) ? { 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 ? { $if smtp_debug ? {
eprintln('\n\n[RECV]') eprintln('\n\n[RECV]')
eprint(str) eprint(str)
@ -129,7 +173,12 @@ fn (mut c Client) send_str(s string) ? {
eprint(s.trim_space()) eprint(s.trim_space())
eprintln('\n[SEND END]') 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] [inline]
@ -138,6 +187,13 @@ fn (mut c Client) send_ehlo() ? {
c.expect_reply(.action_ok) ? 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] [inline]
fn (mut c Client) send_auth() ? { fn (mut c Client) send_auth() ? {
if c.username.len == 0 { if c.username.len == 0 {

View File

@ -8,21 +8,14 @@ fn fn_errors(mut c smtp.Client, m smtp.Mail) bool {
return false return false
} }
/* fn send_mail(starttls bool) {
*
* smtp_test
* Created by: nedimf (07/2020)
*/
fn test_smtp() {
$if !network ? {
return
}
client_cfg := smtp.Client{ client_cfg := smtp.Client{
server: 'smtp.mailtrap.io' server: 'smtp.mailtrap.io'
port: 465
from: 'dev@vlang.io' from: 'dev@vlang.io'
username: os.getenv('VSMTP_TEST_USER') username: os.getenv('VSMTP_TEST_USER')
password: os.getenv('VSMTP_TEST_PASS') password: os.getenv('VSMTP_TEST_PASS')
starttls: starttls
} }
if client_cfg.username == '' && client_cfg.password == '' { if client_cfg.username == '' && client_cfg.password == '' {
eprintln('Please set VSMTP_TEST_USER and VSMTP_TEST_PASS before running this test') eprintln('Please set VSMTP_TEST_USER and VSMTP_TEST_PASS before running this test')
@ -87,3 +80,46 @@ fn test_smtp() {
} }
assert true 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
}