@@ -7,73 +7,16 @@ use std::task::{Context, Poll};
77use std:: time:: Duration ;
88use tokio:: io:: { AsyncBufReadExt , AsyncWriteExt , BufStream } ;
99use tokio:: io:: { AsyncRead , AsyncWrite , ReadBuf } ;
10- use tokio:: task:: { JoinHandle , JoinSet } ;
10+ use tokio:: task:: JoinSet ;
1111use tokio_io_timeout:: TimeoutStream ;
1212use tokio_rustls:: rustls:: client:: ClientSessionMemoryCache ;
1313
1414/// Wraps SMTP connection, contains stream and ESMTP support information.
15- pub struct SmtpConnection < S > {
15+ pub struct SmtpConnection < S : TcpConnect > {
1616 pub stream : BufStream < SmtpStream < S > > ,
1717 pub pipelining : bool ,
1818}
1919
20- /// A connection pool for SMTP connections, keyed by (address, port).
21- ///
22- /// Connections are cached for up to 100 seconds of idle time.
23- ///
24- /// Only a single connection is cached per address/port pair.
25- pub struct SmtpConnectionPool < S >
26- where
27- S : TcpStreamTrait + TcpConnect ,
28- {
29- pool : Arc < retainer:: Cache < ( String , u16 ) , SmtpConnection < S > > > ,
30- monitor_handle : JoinHandle < ( ) > ,
31- context : S :: ConnectionContext ,
32- }
33-
34- impl < S > SmtpConnectionPool < S >
35- where
36- S : TcpStreamTrait + TcpConnect ,
37- {
38- /// Creates a new connection pool and starts the cache monitoring task.
39- pub fn new ( context : S :: ConnectionContext ) -> Arc < Self > {
40- let pool = Arc :: new ( retainer:: Cache :: new ( ) ) ;
41- let pool_clone = pool. clone ( ) ;
42-
43- let monitor_handle =
44- tokio:: spawn ( async move { pool_clone. monitor ( 4 , 0.25 , Duration :: from_secs ( 10 ) ) . await } ) ;
45-
46- Arc :: new ( Self {
47- pool,
48- monitor_handle,
49- context,
50- } )
51- }
52-
53- /// Takes a connection from the pool for the given address and port, if available.
54- pub async fn take ( & self , address : & str , port : u16 ) -> Option < SmtpConnection < S > > {
55- self . pool . remove ( & ( address. to_string ( ) , port) ) . await
56- }
57-
58- /// Puts a connection into the pool for the given address and port, with a 100s timeout.
59- pub async fn put ( & self , address : & str , port : u16 , connection : SmtpConnection < S > ) {
60- // similarly to postfix default -> 100s max idle time.
61- self . pool
62- . insert (
63- ( address. to_string ( ) , port) ,
64- connection,
65- Duration :: from_secs ( 100 ) ,
66- )
67- . await ;
68- }
69- }
70-
71- impl < S : TcpConnect > Drop for SmtpConnectionPool < S > {
72- fn drop ( & mut self ) {
73- self . monitor_handle . abort ( ) ;
74- }
75- }
76-
7720/// A [`TcpStream`] wrapper used for SMTP communication.
7821#[ expect( clippy:: large_enum_variant) ]
7922pub enum SmtpStream < S > {
@@ -242,7 +185,7 @@ where
242185}
243186
244187/// SMTP/LMTP client configuration options.
245- pub struct ClientConfig < ' a > {
188+ pub struct ClientConfig < ' a , S : TcpConnect > {
246189 /// Client hostname used for greeting
247190 pub client_hostname : & ' a str ,
248191
@@ -252,49 +195,61 @@ pub struct ClientConfig<'a> {
252195
253196 /// If `true`, switches to `LHLO` greeting and returns per-recipient composite response.
254197 pub lmtp : bool ,
198+
199+ /// Additional parameters for connection establishment,
200+ /// e.g. a reference to the session transcript channel in the tests
201+ /// (see [`RecTcpStream`]).
202+ ///
203+ /// [`RecTcpStream`]: crate::tcp::rec_stream::RecTcpStream
204+ pub connection_context : S :: ConnectionContext ,
255205}
256206
257207/// Sends an email using an SMTP server at `smtp_addr`.
258208/// If `address` is a domain that resolves to multiple IP addresses,
259209/// all will be tried in parallel and the first successful connection will be used.
260210///
261- /// `pool` is used to reuse existing connections to the same address and port, if available.
211+ /// If `connection` is supplied, it will be used instead,
212+ /// and provided `address` and `port` will only serve as a fallback.
262213pub async fn send < S > (
263214 address : & str ,
264215 port : u16 ,
265216 envelope : & Envelope ,
266- config : ClientConfig < ' _ > ,
217+ config : ClientConfig < ' _ , S > ,
267218 dns_resolver : Arc < TokioResolver > ,
268- pool : Arc < SmtpConnectionPool < S > > ,
219+ connection : & mut Option < SmtpConnection < S > > ,
269220) -> Result < ( ) , crate :: error:: Error >
270221where
271222 S : TcpStreamTrait + TcpConnect ,
272223{
273224 let greeting = if config. lmtp { "LHLO" } else { "EHLO" } ;
274225
275- let ( mut buf_stream, reused, mut pipelining) =
276- if let Some ( connection) = pool. take ( address, port) . await {
277- log:: debug!(
278- "Reusing existing connection to {}" ,
279- connection. stream. get_ref( ) . format_host( address)
226+ let ( mut buf_stream, reused, mut pipelining) = if let Some ( con) = connection. take ( ) {
227+ log:: debug!(
228+ "Reusing existing connection to {}" ,
229+ con. stream. get_ref( ) . format_host( address)
230+ ) ;
231+ if config. tls_config . is_some ( ) {
232+ // This should never happen,
233+ // assert to make sure we never accidentally use a plain connection while expecting TLS.
234+ assert ! (
235+ matches!( con. stream. get_ref( ) , SmtpStream :: Tls ( _) ) ,
236+ "Expected TLS stream from pool, but got plain stream."
280237 ) ;
281- if config. tls_config . is_some ( ) {
282- // This should never happen,
283- // assert to make sure we never accidentally use a plain connection while expecting TLS.
284- assert ! (
285- matches!( connection. stream. get_ref( ) , SmtpStream :: Tls ( _) ) ,
286- "Expected TLS stream from pool, but got plain stream."
287- ) ;
288- }
289- ( connection. stream , true , connection. pipelining )
290- } else {
291- let stream = SmtpStream :: plain (
292- establish_tcp_connection ( address, port, dns_resolver. clone ( ) , pool. context . clone ( ) )
293- . await ?,
294- ) ;
295- log:: debug!( "Successfully connected to {}" , stream. format_host( address) ) ;
296- ( BufStream :: new ( stream) , false , false )
297- } ;
238+ }
239+ ( con. stream , true , con. pipelining )
240+ } else {
241+ let stream = SmtpStream :: plain (
242+ establish_tcp_connection (
243+ address,
244+ port,
245+ dns_resolver. clone ( ) ,
246+ config. connection_context . clone ( ) ,
247+ )
248+ . await ?,
249+ ) ;
250+ log:: debug!( "Successfully connected to {}" , stream. format_host( address) ) ;
251+ ( BufStream :: new ( stream) , false , false )
252+ } ;
298253
299254 let mut response = String :: new ( ) ;
300255
@@ -359,8 +314,13 @@ where
359314 // e.g.: 421 example.org Service closing transmission channel - command timeout
360315 if response. starts_with ( "421" ) {
361316 log:: debug!( "Reused connection is dead; establishing new connection..." ) ;
362- let stream: S =
363- establish_tcp_connection ( address, port, dns_resolver, pool. context . clone ( ) ) . await ?;
317+ let stream: S = establish_tcp_connection (
318+ address,
319+ port,
320+ dns_resolver,
321+ config. connection_context . clone ( ) ,
322+ )
323+ . await ?;
364324 log:: debug!( "Successfully connected to {}" , stream. peer_addr( ) ?) ;
365325 buf_stream = BufStream :: new ( SmtpStream :: plain ( stream) ) ;
366326 false
@@ -497,15 +457,10 @@ where
497457 smtp_read ! ( "end of DATA" , "250" ) ?;
498458 }
499459
500- pool. put (
501- address,
502- port,
503- SmtpConnection {
504- stream : buf_stream,
505- pipelining,
506- } ,
507- )
508- . await ;
460+ connection. replace ( SmtpConnection {
461+ stream : buf_stream,
462+ pipelining,
463+ } ) ;
509464
510465 Ok ( ( ) )
511466}
0 commit comments