tvl-depot/users/Profpatsch/imap-idle.rs
Profpatsch d053abfd2a feat(users/Profpatsch): add imap-idle
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>
2021-03-22 22:52:11 +00:00

132 lines
4.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 dont 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`,
/// wed break any UCSPI client programs that *dont* 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 dont 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());
}
}