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 {
|
$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
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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
|
||||||
|
@ -31,6 +32,7 @@ pub enum BodyType {
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
mut:
|
mut:
|
||||||
conn net.TcpConn
|
conn net.TcpConn
|
||||||
|
ssl_conn &openssl.SSLConn = 0
|
||||||
reader io.BufferedReader
|
reader io.BufferedReader
|
||||||
pub:
|
pub:
|
||||||
server string
|
server string
|
||||||
|
@ -38,8 +40,11 @@ pub:
|
||||||
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
|
||||||
|
|
||||||
|
if c.ssl {
|
||||||
|
c.connect_ssl() ?
|
||||||
|
} else {
|
||||||
c.reader = io.new_buffered_reader(reader: c.conn)
|
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) ?
|
||||||
|
if c.encrypted {
|
||||||
|
c.ssl_conn.shutdown() ?
|
||||||
|
} else {
|
||||||
c.conn.close() ?
|
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,8 +173,13 @@ fn (mut c Client) send_str(s string) ? {
|
||||||
eprint(s.trim_space())
|
eprint(s.trim_space())
|
||||||
eprintln('\n[SEND END]')
|
eprintln('\n[SEND END]')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.encrypted {
|
||||||
|
c.ssl_conn.write(s.bytes()) ?
|
||||||
|
} else {
|
||||||
c.conn.write(s.bytes()) ?
|
c.conn.write(s.bytes()) ?
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[inline]
|
[inline]
|
||||||
fn (mut c Client) send_ehlo() ? {
|
fn (mut c Client) send_ehlo() ? {
|
||||||
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue