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 {
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

View File

@ -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',

View File

@ -6,6 +6,7 @@ module smtp
* Created by: nedimf (07/2020)
*/
import net
import net.openssl
import encoding.base64
import strings
import time
@ -31,6 +32,7 @@ pub enum BodyType {
pub struct Client {
mut:
conn net.TcpConn
ssl_conn &openssl.SSLConn = 0
reader io.BufferedReader
pub:
server string
@ -38,8 +40,11 @@ pub:
username string
password string
from string
ssl bool
starttls bool
pub mut:
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
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) ?
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,8 +173,13 @@ fn (mut c Client) send_str(s string) ? {
eprint(s.trim_space())
eprintln('\n[SEND END]')
}
if c.encrypted {
c.ssl_conn.write(s.bytes()) ?
} else {
c.conn.write(s.bytes()) ?
}
}
[inline]
fn (mut c Client) send_ehlo() ? {
@ -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 {

View File

@ -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
}