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: 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(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(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: message_count, maildrop_size: 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 { 
173                message_id: message_id, 
174                message_size: message_size
175            });
176        }
177
178        Ok(result)
179    }
180
181    /// Returns the size of a given message.
182    ///
183    /// # Arguments
184    ///
185    /// * `message_id` - id of the message to query
186    pub fn get_message_size(&mut self, message_id: u32) -> Result<u32, Box<dyn Error>> {
187        let line = self.invoke_single_line(&format!("LIST {}\r\n", message_id))?;
188        let mut info = line.split(' ');
189        let _ = info.next();    // skip "+OK"
190        let _ = info.next();    // skip message id
191        let message_size = info.next().ok_or("missing size")?.parse::<u32>()?;
192     
193        Ok(message_size)
194    }
195
196    /// Downloads a given message.
197    ///
198    /// # Arguments
199    ///
200    /// * `message_id` - id of the message to download
201    /// * `writer`     - writer to store message
202    pub fn retrieve(&mut self, message_id: u32, writer: &mut impl Write) -> Result<(), Box<dyn Error>> {
203        let lines = self.invoke_multi_line(&format!("RETR {}\r\n", message_id))?;
204        for line in lines {
205            writer.write(line.as_bytes())?;
206            writer.write(b"\n")?;
207        }
208
209        Ok(())
210    }
211
212    /// Deletes a given message.
213    ///
214    /// # Arguments
215    ///
216    /// * `message_id` - id of the message to download
217    pub fn delete(&mut self, message_id: u32) -> Result<(), Box<dyn Error>> {
218        self.invoke_single_line(&format!("DELE {}\r\n", message_id))?;
219        Ok(())
220    }
221
222    /// Unmark any messages marked as delete.
223    pub fn reset(&mut self) -> Result<(), Box<dyn Error>> {
224        self.invoke_single_line("RSET\r\n")?;
225        Ok(())
226    }
227
228    /// Returns the message header an a given number of lines from the message.
229    ///
230    /// # Arguments
231    ///
232    /// * `message_id` - id of the message
233    /// * `line_count` - count of lines to return from the message body
234    pub fn top(&mut self, message_id: u32, line_count: u32) -> Result<String, Box<dyn Error>> {
235        let lines = self.invoke_multi_line(&format!("TOP {} {}\r\n", message_id, line_count))?;
236        let mut message = String::new();
237        for line in lines {
238            message.push_str(&line);
239            message.push('\n');
240        }
241
242        Ok(message)
243    }
244
245    /// Returns the unique ids of all messages.
246    pub fn list_unique_ids(&mut self) -> Result<Vec<Pop3MessageUidInfo>, Box<dyn Error>> {
247        let lines = self.invoke_multi_line("UIDL\r\n")?;
248        let mut result = vec!();
249
250        for line in lines {
251            let mut info = line.split(' ');
252            let message_id = info.next().ok_or("missing id")?.parse::<u32>()?;
253            let unique_id = info.next().ok_or("missing unique id")?.to_string();
254
255            result.push(Pop3MessageUidInfo { message_id: message_id, unique_id: unique_id });
256        }
257
258        Ok(result)
259    }
260
261    /// Returns the unique id of a given message.
262    ///
263    /// # Arguments
264    ///
265    /// * `message_id` - id of the message
266    pub fn get_unique_id(&mut self, message_id :u32) -> Result<String, Box<dyn Error>> {
267        let line = self.invoke_single_line(&format!("UIDL {}\r\n", message_id))?;
268        let mut info = line.split(' ');
269        let _ = info.next(); // skip "+OK"
270        let _ = info.next(); // skip message id
271        let unique_id = info.next().ok_or("missing unique id")?.to_string();
272
273        Ok(unique_id)
274    }
275}
276
277impl Drop for Pop3Connection {
278    /// Closes POP3 connection on drop.
279    fn drop(&mut self) {
280        let _ = self.invoke_single_line("QUIT\r\n");
281    }
282}