lettre/transport/smtp/client/
connection.rs1use std::{
2 fmt::Display,
3 io::{self, BufRead, BufReader, Write},
4 net::{IpAddr, ToSocketAddrs},
5 time::Duration,
6};
7
8#[cfg(feature = "tracing")]
9use super::escape_crlf;
10use super::{ClientCodec, NetworkStream, TlsParameters};
11use crate::{
12 address::Envelope,
13 transport::smtp::{
14 authentication::{Credentials, Mechanism},
15 commands::{Auth, Data, Ehlo, Mail, Noop, Quit, Rcpt, Starttls},
16 error,
17 error::Error,
18 extension::{ClientId, Extension, MailBodyParameter, MailParameter, ServerInfo},
19 response::{parse_response, Response},
20 },
21};
22
23macro_rules! try_smtp (
24 ($err: expr, $client: ident) => ({
25 match $err {
26 Ok(val) => val,
27 Err(err) => {
28 $client.abort();
29 return Err(From::from(err))
30 },
31 }
32 })
33);
34
35pub struct SmtpConnection {
37 stream: BufReader<NetworkStream>,
40 panic: bool,
42 server_info: ServerInfo,
44}
45
46impl SmtpConnection {
47 pub fn server_info(&self) -> &ServerInfo {
49 &self.server_info
50 }
51
52 pub fn connect<A: ToSocketAddrs>(
58 server: A,
59 timeout: Option<Duration>,
60 hello_name: &ClientId,
61 tls_parameters: Option<&TlsParameters>,
62 local_address: Option<IpAddr>,
63 ) -> Result<SmtpConnection, Error> {
64 let stream = NetworkStream::connect(server, timeout, tls_parameters, local_address)?;
65 let stream = BufReader::new(stream);
66 let mut conn = SmtpConnection {
67 stream,
68 panic: false,
69 server_info: ServerInfo::default(),
70 };
71 conn.set_timeout(timeout).map_err(error::network)?;
72 let _response = conn.read_response()?;
74
75 conn.ehlo(hello_name)?;
76
77 #[cfg(feature = "tracing")]
79 tracing::debug!("server {}", conn.server_info);
80 Ok(conn)
81 }
82
83 pub fn send(&mut self, envelope: &Envelope, email: &[u8]) -> Result<Response, Error> {
84 let mut mail_options = vec![];
86
87 if envelope.has_non_ascii_addresses() {
94 if !self.server_info().supports_feature(Extension::SmtpUtfEight) {
95 return Err(error::client(
97 "Envelope contains non-ascii chars but server does not support SMTPUTF8",
98 ));
99 }
100 mail_options.push(MailParameter::SmtpUtfEight);
101 }
102
103 if !email.is_ascii() {
105 if !self.server_info().supports_feature(Extension::EightBitMime) {
106 return Err(error::client(
107 "Message contains non-ascii chars but server does not support 8BITMIME",
108 ));
109 }
110 mail_options.push(MailParameter::Body(MailBodyParameter::EightBitMime));
111 }
112
113 try_smtp!(
114 self.command(Mail::new(envelope.from().cloned(), mail_options)),
115 self
116 );
117
118 for to_address in envelope.to() {
120 try_smtp!(self.command(Rcpt::new(to_address.clone(), vec![])), self);
121 }
122
123 try_smtp!(self.command(Data), self);
125
126 let result = try_smtp!(self.message(email), self);
128 Ok(result)
129 }
130
131 pub fn has_broken(&self) -> bool {
132 self.panic
133 }
134
135 pub fn can_starttls(&self) -> bool {
136 !self.is_encrypted() && self.server_info.supports_feature(Extension::StartTls)
137 }
138
139 #[allow(unused_variables)]
140 pub fn starttls(
141 &mut self,
142 tls_parameters: &TlsParameters,
143 hello_name: &ClientId,
144 ) -> Result<(), Error> {
145 if self.server_info.supports_feature(Extension::StartTls) {
146 #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
147 {
148 try_smtp!(self.command(Starttls), self);
149 self.stream.get_mut().upgrade_tls(tls_parameters)?;
150 #[cfg(feature = "tracing")]
151 tracing::debug!("connection encrypted");
152 try_smtp!(self.ehlo(hello_name), self);
154 Ok(())
155 }
156 #[cfg(not(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))]
157 unreachable!("TLS support required but not supported");
160 } else {
161 Err(error::client("STARTTLS is not supported on this server"))
162 }
163 }
164
165 fn ehlo(&mut self, hello_name: &ClientId) -> Result<(), Error> {
167 let ehlo_response = try_smtp!(self.command(Ehlo::new(hello_name.clone())), self);
168 self.server_info = try_smtp!(ServerInfo::from_response(&ehlo_response), self);
169 Ok(())
170 }
171
172 pub fn quit(&mut self) -> Result<Response, Error> {
173 Ok(try_smtp!(self.command(Quit), self))
174 }
175
176 pub fn abort(&mut self) {
177 if !self.panic {
179 self.panic = true;
180 let _ = self.command(Quit);
181 }
182 let _ = self.stream.get_mut().shutdown(std::net::Shutdown::Both);
183 }
184
185 pub fn set_stream(&mut self, stream: NetworkStream) {
187 self.stream = BufReader::new(stream);
188 }
189
190 pub fn is_encrypted(&self) -> bool {
192 self.stream.get_ref().is_encrypted()
193 }
194
195 pub fn set_timeout(&mut self, duration: Option<Duration>) -> io::Result<()> {
197 self.stream.get_mut().set_read_timeout(duration)?;
198 self.stream.get_mut().set_write_timeout(duration)
199 }
200
201 pub fn test_connected(&mut self) -> bool {
203 self.command(Noop).is_ok()
204 }
205
206 pub fn auth(
208 &mut self,
209 mechanisms: &[Mechanism],
210 credentials: &Credentials,
211 ) -> Result<Response, Error> {
212 let mechanism = self
213 .server_info
214 .get_auth_mechanism(mechanisms)
215 .ok_or_else(|| error::client("No compatible authentication mechanism was found"))?;
216
217 let mut challenges = 10;
219 let mut response = self.command(Auth::new(mechanism, credentials.clone(), None)?)?;
220
221 while challenges > 0 && response.has_code(334) {
222 challenges -= 1;
223 response = try_smtp!(
224 self.command(Auth::new_from_response(
225 mechanism,
226 credentials.clone(),
227 &response,
228 )?),
229 self
230 );
231 }
232
233 if challenges == 0 {
234 Err(error::response("Unexpected number of challenges"))
235 } else {
236 Ok(response)
237 }
238 }
239
240 pub fn message(&mut self, message: &[u8]) -> Result<Response, Error> {
242 let mut codec = ClientCodec::new();
243 let mut out_buf = Vec::with_capacity(message.len());
244 codec.encode(message, &mut out_buf);
245 self.write(out_buf.as_slice())?;
246 self.write(b"\r\n.\r\n")?;
247
248 self.read_response()
249 }
250
251 pub fn command<C: Display>(&mut self, command: C) -> Result<Response, Error> {
253 self.write(command.to_string().as_bytes())?;
254 self.read_response()
255 }
256
257 fn write(&mut self, string: &[u8]) -> Result<(), Error> {
259 self.stream
260 .get_mut()
261 .write_all(string)
262 .map_err(error::network)?;
263 self.stream.get_mut().flush().map_err(error::network)?;
264
265 #[cfg(feature = "tracing")]
266 tracing::debug!("Wrote: {}", escape_crlf(&String::from_utf8_lossy(string)));
267 Ok(())
268 }
269
270 pub fn read_response(&mut self) -> Result<Response, Error> {
272 let mut buffer = String::with_capacity(100);
273
274 while self.stream.read_line(&mut buffer).map_err(error::network)? > 0 {
275 #[cfg(feature = "tracing")]
276 tracing::debug!("<< {}", escape_crlf(&buffer));
277 match parse_response(&buffer) {
278 Ok((_remaining, response)) => {
279 return if response.is_positive() {
280 Ok(response)
281 } else {
282 Err(error::code(
283 response.code(),
284 Some(response.message().collect()),
285 ))
286 };
287 }
288 Err(nom::Err::Failure(e)) => {
289 return Err(error::response(e.to_string()));
290 }
291 Err(nom::Err::Incomplete(_)) => { }
292 Err(nom::Err::Error(e)) => {
293 return Err(error::response(e.to_string()));
294 }
295 }
296 }
297
298 Err(error::response("incomplete response"))
299 }
300
301 #[cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls"))]
303 #[cfg_attr(
304 docsrs,
305 doc(cfg(any(feature = "native-tls", feature = "rustls", feature = "boring-tls")))
306 )]
307 pub fn peer_certificate(&self) -> Result<Vec<u8>, Error> {
308 self.stream.get_ref().peer_certificate()
309 }
310
311 #[cfg(feature = "boring-tls")]
323 #[cfg_attr(docsrs, doc(cfg(feature = "boring-tls")))]
324 pub fn tls_verify_result(&self) -> Result<(), Error> {
325 self.stream.get_ref().tls_verify_result()
326 }
327
328 #[cfg(any(feature = "rustls", feature = "boring-tls"))]
330 #[cfg_attr(docsrs, doc(cfg(any(feature = "rustls", feature = "boring-tls"))))]
331 pub fn certificate_chain(&self) -> Result<Vec<Vec<u8>>, Error> {
332 self.stream.get_ref().certificate_chain()
333 }
334}