extern crate exec_helpers; // extern crate arglib_netencode; // extern crate netencode; extern crate epoll; extern crate imap; // use netencode::dec; use imap::extensions::idle::SetReadTimeout; use std::convert::TryFrom; use std::fs::File; use std::io::{Read, Write}; use std::os::unix::io::{AsRawFd, FromRawFd, RawFd}; use std::time::Duration; /// Implements an UCSPI client that wraps fd 6 & 7 /// and implements Write and Read with a timeout. /// See https://cr.yp.to/proto/ucspi.txt #[derive(Debug)] struct UcspiClient { read: File, read_epoll_fd: RawFd, read_timeout: Option, write: File, } impl UcspiClient { /// Use fd 6 and 7 to connect to the net, as is specified. /// Unsafe because fd 6 and 7 are global resources and we don’t mutex them. pub unsafe fn new_from_6_and_7() -> std::io::Result { unsafe { let read_epoll_fd = epoll::create(false)?; Ok(UcspiClient { read: File::from_raw_fd(6), read_epoll_fd, read_timeout: None, write: File::from_raw_fd(7), }) } } } /// Emulates set_read_timeout() like on a TCP socket with an epoll on read. /// The BSD socket API is rather bad, so fd != fd, /// and if we cast the `UcspiClient` fds to `TcpStream` instead of `File`, /// we’d break any UCSPI client programs that *don’t* connect to TCP. /// Instead we use the (linux) `epoll` API in read to wait on the timeout. impl SetReadTimeout for UcspiClient { fn set_read_timeout(&mut self, timeout: Option) -> imap::Result<()> { self.read_timeout = timeout; Ok(()) } } impl Read for UcspiClient { // TODO: test the epoll code with a short timeout fn read(&mut self, buf: &mut [u8]) -> std::io::Result { const NO_DATA: u64 = 0; // in order to implement the read_timeout, // we use epoll to wait for either data or time out epoll::ctl( self.read_epoll_fd, epoll::ControlOptions::EPOLL_CTL_ADD, self.read.as_raw_fd(), epoll::Event::new(epoll::Events::EPOLLIN, NO_DATA), )?; let UNUSED = epoll::Event::new(epoll::Events::EPOLLIN, NO_DATA); let wait = epoll::wait( self.read_epoll_fd, match self.read_timeout { Some(duration) => { i32::try_from(duration.as_millis()).expect("duration too big for epoll") } None => -1, // infinite }, // event that was generated; but we don’t care &mut vec![UNUSED; 1][..], ); // Delete the listen fd from the epoll fd before reacting // (otherwise it fails on the next read with `EPOLL_CTL_ADD`) epoll::ctl( self.read_epoll_fd, epoll::ControlOptions::EPOLL_CTL_DEL, self.read.as_raw_fd(), UNUSED, )?; match wait { // timeout happened (0 events) Ok(0) => Err(std::io::Error::new( std::io::ErrorKind::TimedOut, "ucspi read timeout", )), // its ready for reading, we can read Ok(_) => self.read.read(buf), // error err => err, } } } /// Just proxy through the `Write` of the write fd. impl Write for UcspiClient { fn write(&mut self, buf: &[u8]) -> std::io::Result { self.write.write(buf) } fn flush(&mut self) -> std::io::Result<()> { self.write.flush() } } /// Connect to IMAP account and listen for new mails on the INBOX. fn main() { exec_helpers::no_args("imap-idle"); // TODO: use arglib_netencode let username = std::env::var("IMAP_USERNAME").expect("username"); let password = std::env::var("IMAP_PASSWORD").expect("password"); let net = unsafe { UcspiClient::new_from_6_and_7().expect("no ucspi client for you") }; let client = imap::Client::new(net); let mut session = client .login(username, password) .map_err(|(err, _)| err) .expect("unable to login"); eprintln!("{:#?}", session); let list = session.list(None, Some("*")); eprintln!("{:#?}", list); let mailbox = session.examine("INBOX"); eprintln!("{:#?}", mailbox); fn now() -> String { String::from_utf8_lossy(&std::process::Command::new("date").output().unwrap().stdout) .trim_right() .to_string() } loop { eprintln!("{}: idling on INBOX", now()); let mut handle = session.idle().expect("cannot idle on INBOX"); let () = handle.wait_keepalive().expect("waiting on idle failed"); eprintln!("{}: The mailbox has changed!", now()); } }