rust_pop3_client/
lib.rs

1mod line_reader;
2
3use std::sync::Arc;
4use std::net::TcpStream;
5use std::error::Error;
6use std::io::{Write};
7
8use rustls::{RootCertStore, ClientConnection, StreamOwned};
9
10use line_reader::LineReader;
11
12/// POP3 connection
13pub struct Pop3Connection {    
14    tls: StreamOwned<ClientConnection, TcpStream>,
15    reader: LineReader,
16}
17
18/// POP3 maildrop statistics
19pub struct Pop3Stat {
20    /// count of massages in the maildrop
21    pub message_count: u32,
22
23    /// size of the maildrop in bytes
24    pub maildrop_size: u32,
25}
26
27/// POP3 message info
28pub struct Pop3MessageInfo {
29    /// numerical Id of the message used for various commands
30    pub message_id: u32,
31
32    /// size of the message in bytes
33    pub message_size: u32,
34}
35
36/// POP3 message unique id info
37pub struct Pop3MessageUidInfo {
38    /// numerical Id of the message used for various commands
39    pub message_id: u32,
40
41    // unique id of the message
42    pub unique_id: String,
43}
44
45impl Pop3Connection {
46
47    /// Returns a new POP3 connection.
48    ///
49    /// # Arguments
50    ///
51    /// * `host` - IP-Address or host name of the POP3 server to connect
52    /// * `port` - Port of the POP3 server to connect
53    pub fn new(host: &str, port: u16) -> Result<Pop3Connection, Box<dyn Error>> {
54        let mut root_store = RootCertStore::empty();
55        for cert in rustls_native_certs::load_native_certs()? {
56            root_store.add(&rustls::Certificate(cert.0))?;
57        }
58
59        Pop3Connection::with_custom_certs(host, port, root_store)
60    }
61
62    /// Returns a new POP3 connection with custom certificates.
63    ///
64    /// # Arguments
65    ///
66    /// * `host` - IP-Address or host name of the POP3 server to connect
67    /// * `port` - Port of the POP3 server to connect
68    /// * `root_store` - Store of trusted (root) certificates.
69    ///
70    /// # Examples
71    ///
72    /// ```
73    /// use rust_pop3_client::Pop3Connection;
74    /// use rustls::RootCertStore;
75    ///
76    /// let mut root_store = RootCertStore::empty();
77    /// for cert in rustls_native_certs::load_native_certs().unwrap() {
78    ///     root_store.add(&rustls::Certificate(cert.0)).unwrap();
79    /// }
80    /// 
81    /// let connection = Pop3Connection::with_custom_certs("", 995, root_store);
82    /// ```
83    pub fn with_custom_certs(host: &str, port: u16, root_store: RootCertStore) -> Result<Pop3Connection, Box<dyn Error>> {
84        let config = rustls::ClientConfig::builder()
85            .with_safe_defaults()
86            .with_root_certificates(root_store)
87            .with_no_client_auth();
88
89        let server_name = host.try_into()?;
90
91        let connection = rustls::ClientConnection::new(Arc::new(config), server_name)?;
92        let stream =  TcpStream::connect(format!("{}:{}", host, port))?;
93        let tls = rustls::StreamOwned::new(connection, stream);
94
95        let mut client = Pop3Connection { 
96            tls,
97            reader: LineReader::new()
98        };
99
100        client.read_status_line()?;
101        Ok(client)
102    }
103
104    fn read_status_line(&mut self) -> Result<String, Box<dyn Error>> {
105        let line = self.reader.read_line(&mut self.tls)?;
106
107        match line.starts_with("+OK") {
108            true => Ok(line),
109            _ => Err(line.into())
110        }
111    }
112
113    fn invoke_single_line(&mut self, command: &str) -> Result<String, Box<dyn Error>> {
114        self.tls.write_all(command.as_bytes())?;
115        self.read_status_line()
116    }
117
118    fn invoke_multi_line(&mut self, command: &str) -> Result<Vec<String>, Box<dyn Error>> {
119        self.tls.write_all(command.as_bytes())?;
120        self.read_status_line()?;
121
122        let mut response : Vec<String> = vec!();
123        loop {
124            let line = self.reader.read_line(&mut self.tls)?;
125            match line {
126                _ if line == "." => { break },
127                _ if line.starts_with(".") => { response.push(line[1..].to_string()); },
128                _ => { response.push(line); }
129            };
130        }
131
132        Ok(response)
133    }
134
135    /// Authenticate a POP3 session using username and password.
136    ///
137    /// This is usually the first set of commands after a POP3 session
138    /// is established.
139    ///
140    /// # Arguments
141    ///
142    /// * `user`     - Name of the user, typically it's e-mail address.
143    /// * `password` - Password of the user. 
144    pub fn login(&mut self, user: &str, password: &str) -> Result<(), Box<dyn Error>> {
145        self.invoke_single_line(&format!("USER {}\r\n", user))?;
146        self.invoke_single_line(&format!("PASS {}\r\n", password))?;
147        Ok(())
148    }
149
150    /// Returns maildrop statistics.
151    pub fn stat(&mut self) -> Result<Pop3Stat, Box<dyn Error>> {
152        let stat = self.invoke_single_line("STAT\r\n")?;
153        let mut stat = stat.split(' ');
154        let _ = stat.next();
155        let message_count = stat.next().ok_or("missing message count")?;
156        let message_count = message_count.parse::<u32>()?;
157        let maildrop_size = stat.next().ok_or("missing maildrop size")?;
158        let maildrop_size = maildrop_size.parse::<u32>()?;
159
160        Ok(Pop3Stat { message_count, maildrop_size })
161    }
162
163    /// Returns id and size of each message.
164    pub fn list(&mut self) -> Result<Vec<Pop3MessageInfo>, Box<dyn Error>> {
165        let lines = self.invoke_multi_line("LIST\r\n")?;
166        let mut result = vec!();
167        for line in lines {
168            let mut info = line.split(' ');
169            let message_id = info.next().ok_or("missing id")?.parse::<u32>()?;
170            let message_size = info.next().ok_or("missing size")?.parse::<u32>()?;
171
172            result.push(Pop3MessageInfo { message_id, message_size });
173        }
174
175        Ok(result)
176    }
177
178    /// Returns the size of a given message.
179    ///
180    /// # Arguments
181    ///
182    /// * `message_id` - id of the message to query
183    pub fn get_message_size(&mut self, message_id: u32) -> Result<u32, Box<dyn Error>> {
184        let line = self.invoke_single_line(&format!("LIST {}\r\n", message_id))?;
185        let mut info = line.split(' ');
186        let _ = info.next();    // skip "+OK"
187        let _ = info.next();    // skip message id
188        let message_size = info.next().ok_or("missing size")?.parse::<u32>()?;
189     
190        Ok(message_size)
191    }
192
193    /// Downloads a given message.
194    ///
195    /// # Arguments
196    ///
197    /// * `message_id` - id of the message to download
198    /// * `writer`     - writer to store message
199    pub fn retrieve(&mut self, message_id: u32, writer: &mut impl Write) -> Result<(), Box<dyn Error>> {
200        let lines = self.invoke_multi_line(&format!("RETR {}\r\n", message_id))?;
201        for line in lines {
202            writer.write_all(line.as_bytes())?;
203            writer.write_all(b"\n")?;
204        }
205
206        Ok(())
207    }
208
209    /// Deletes a given message.
210    ///
211    /// # Arguments
212    ///
213    /// * `message_id` - id of the message to download
214    pub fn delete(&mut self, message_id: u32) -> Result<(), Box<dyn Error>> {
215        self.invoke_single_line(&format!("DELE {}\r\n", message_id))?;
216        Ok(())
217    }
218
219    /// Unmark any messages marked as delete.
220    pub fn reset(&mut self) -> Result<(), Box<dyn Error>> {
221        self.invoke_single_line("RSET\r\n")?;
222        Ok(())
223    }
224
225    /// Returns the message header an a given number of lines from the message.
226    ///
227    /// # Arguments
228    ///
229    /// * `message_id` - id of the message
230    /// * `line_count` - count of lines to return from the message body
231    pub fn top(&mut self, message_id: u32, line_count: u32) -> Result<String, Box<dyn Error>> {
232        let lines = self.invoke_multi_line(&format!("TOP {} {}\r\n", message_id, line_count))?;
233        let mut message = String::new();
234        for line in lines {
235            message.push_str(&line);
236            message.push('\n');
237        }
238
239        Ok(message)
240    }
241
242    /// Returns the unique ids of all messages.
243    pub fn list_unique_ids(&mut self) -> Result<Vec<Pop3MessageUidInfo>, Box<dyn Error>> {
244        let lines = self.invoke_multi_line("UIDL\r\n")?;
245        let mut result = vec!();
246
247        for line in lines {
248            let mut info = line.split(' ');
249            let message_id = info.next().ok_or("missing id")?.parse::<u32>()?;
250            let unique_id = info.next().ok_or("missing unique id")?.to_string();
251
252            result.push(Pop3MessageUidInfo { message_id, unique_id });
253        }
254
255        Ok(result)
256    }
257
258    /// Returns the unique id of a given message.
259    ///
260    /// # Arguments
261    ///
262    /// * `message_id` - id of the message
263    pub fn get_unique_id(&mut self, message_id :u32) -> Result<String, Box<dyn Error>> {
264        let line = self.invoke_single_line(&format!("UIDL {}\r\n", message_id))?;
265        let mut info = line.split(' ');
266        let _ = info.next(); // skip "+OK"
267        let _ = info.next(); // skip message id
268        let unique_id = info.next().ok_or("missing unique id")?.to_string();
269
270        Ok(unique_id)
271    }
272}
273
274impl Drop for Pop3Connection {
275    /// Closes POP3 connection on drop.
276    fn drop(&mut self) {
277        let _ = self.invoke_single_line("QUIT\r\n");
278    }
279}