diff options
author | Profpatsch <mail@profpatsch.de> | 2021-03-22T00·08+0100 |
---|---|---|
committer | Profpatsch <mail@profpatsch.de> | 2021-03-22T22·52+0000 |
commit | d053abfd2a52d663fb31bf15b910cd7145abc285 (patch) | |
tree | f8f00503ebe30fe1b4038f6635860f64b4b2de5e /users/Profpatsch/imap-idle.rs | |
parent | a747b46f196c1faee6affc9c53c7d0371148c8fe (diff) |
feat(users/Profpatsch): add imap-idle r/2319
A small UCSPI client which connects to an IMAP server, authenticates with username and password (for Christ’s sake, put it in `s6-tlsclient`), selects the `INBOX` and proceeds to listen for new mails. Later it will generate an event on stdout and to be used for push messaging and triggering a full `mbsync` run on new message. Currently I’m testing it via ``` env CAFILE=/run/current-system/etc/ssl/certs/ca-bundle.crt \ IMAP_USERNAME=<username> \ backtick -i IMAP_PASSWORD ' pass' ' <password-entry>' '' \ s6-tlsclient -v <imap-server> 993 ./result ``` Change-Id: I221717d374c0efc8d9e05fe0dfccba31798b3c5c Reviewed-on: https://cl.tvl.fyi/c/depot/+/2636 Tested-by: BuildkiteCI Reviewed-by: Profpatsch <mail@profpatsch.de>
Diffstat (limited to 'users/Profpatsch/imap-idle.rs')
-rw-r--r-- | users/Profpatsch/imap-idle.rs | 132 |
1 files changed, 132 insertions, 0 deletions
diff --git a/users/Profpatsch/imap-idle.rs b/users/Profpatsch/imap-idle.rs new file mode 100644 index 000000000000..9dce736d0d8a --- /dev/null +++ b/users/Profpatsch/imap-idle.rs @@ -0,0 +1,132 @@ +extern crate exec_helpers; +// extern crate arglib_netencode; +// extern crate netencode; +extern crate imap; +extern crate epoll; + +// use netencode::dec; +use std::convert::TryFrom; +use std::io::{Read, Write}; +use std::fs::File; +use std::os::unix::io::{FromRawFd, AsRawFd, RawFd}; +use std::time::Duration; +use imap::extensions::idle::SetReadTimeout; + +/// 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<Duration>, + 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<Self> { + 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<Duration>) -> 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<usize> { + 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<usize> { + 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()); + } +} |