From 6d2a88e31f7a90461579818daf0ba12182554a72 Mon Sep 17 00:00:00 2001 From: starryskye Date: Wed, 16 Feb 2022 00:18:51 -0700 Subject: [PATCH] net.smtp: add STARTTLS and implicit SSL support (#13473) --- cmd/tools/modules/testing/common.v | 1 + cmd/tools/vtest-self.v | 3 ++ vlib/net/smtp/smtp.v | 72 ++++++++++++++++++++++++++---- vlib/net/smtp/smtp_test.v | 56 ++++++++++++++++++----- 4 files changed, 114 insertions(+), 18 deletions(-) diff --git a/cmd/tools/modules/testing/common.v b/cmd/tools/modules/testing/common.v index ac34462065..f80f0c9928 100644 --- a/cmd/tools/modules/testing/common.v +++ b/cmd/tools/modules/testing/common.v @@ -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 diff --git a/cmd/tools/vtest-self.v b/cmd/tools/vtest-self.v index 82d6e0cb4a..c6db302120 100644 --- a/cmd/tools/vtest-self.v +++ b/cmd/tools/vtest-self.v @@ -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', diff --git a/vlib/net/smtp/smtp.v b/vlib/net/smtp/smtp.v index 6e310cd1a1..9b3d7a8a28 100644 --- a/vlib/net/smtp/smtp.v +++ b/vlib/net/smtp/smtp.v @@ -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 { diff --git a/vlib/net/smtp/smtp_test.v b/vlib/net/smtp/smtp_test.v index d975e57c43..9c4732654a 100644 --- a/vlib/net/smtp/smtp_test.v +++ b/vlib/net/smtp/smtp_test.v @@ -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 +}