From d053abfd2a52d663fb31bf15b910cd7145abc285 Mon Sep 17 00:00:00 2001 From: Profpatsch Date: Mon, 22 Mar 2021 01:08:24 +0100 Subject: feat(users/Profpatsch): add imap-idle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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= \ backtick -i IMAP_PASSWORD ' pass' ' ' '' \ s6-tlsclient -v 993 ./result ``` Change-Id: I221717d374c0efc8d9e05fe0dfccba31798b3c5c Reviewed-on: https://cl.tvl.fyi/c/depot/+/2636 Tested-by: BuildkiteCI Reviewed-by: Profpatsch --- users/Profpatsch/imap-idle.nix | 14 +++++ users/Profpatsch/imap-idle.rs | 132 +++++++++++++++++++++++++++++++++++++++ users/Profpatsch/rust-crates.nix | 89 ++++++++++++++++++++++++++ 3 files changed, 235 insertions(+) create mode 100644 users/Profpatsch/imap-idle.nix create mode 100644 users/Profpatsch/imap-idle.rs diff --git a/users/Profpatsch/imap-idle.nix b/users/Profpatsch/imap-idle.nix new file mode 100644 index 0000000000..30c7b6e8aa --- /dev/null +++ b/users/Profpatsch/imap-idle.nix @@ -0,0 +1,14 @@ +{ depot, pkgs, lib, ... }: + +let + imap-idle = depot.users.Profpatsch.writers.rustSimple { + name = "imap-idle"; + dependencies = [ + depot.users.Profpatsch.arglib.netencode.rust + depot.users.Profpatsch.rust-crates.imap + depot.users.Profpatsch.rust-crates.epoll + depot.users.Profpatsch.execline.exec-helpers + ]; + } (builtins.readFile ./imap-idle.rs); + +in imap-idle diff --git a/users/Profpatsch/imap-idle.rs b/users/Profpatsch/imap-idle.rs new file mode 100644 index 0000000000..9dce736d0d --- /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, + 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()); + } +} diff --git a/users/Profpatsch/rust-crates.nix b/users/Profpatsch/rust-crates.nix index e6a191f70f..7004a699c4 100644 --- a/users/Profpatsch/rust-crates.nix +++ b/users/Profpatsch/rust-crates.nix @@ -119,4 +119,93 @@ rec { features = [ "std" "alloc" ]; }; + base64 = pkgs.buildRustCrate { + pname = "base64"; + version = "0.13.0"; + crateName = "base64"; + edition = "2018"; + sha256 = "0i0jk5sgq37kc4c90d1g7dp7zvphbg0dbqc1ajnn0vffjxblgamg"; + features = [ "alloc" "std" ]; + }; + + bufstream = pkgs.buildRustCrate { + pname = "bufstream"; + version = "0.1.4"; + crateName = "bufstream"; + sha256 = "10rqm7jly5jjx7wcc19q6q4m2zsrw3l2v3m1054wnbwvdh42xxf1"; + }; + + autocfg = pkgs.buildRustCrate { + pname = "autocfg"; + version = "1.0.1"; + crateName = "autocfg"; + sha256 = "1lsjz23jdcchcqbsmlzd4iksg3hc8bdvy677jy0938i2gp24frw1"; + }; + + num-traits = pkgs.buildRustCrate { + pname = "num-traits"; + version = "0.2.14"; + crateName = "num-traits"; + buildDependencies = [ autocfg ]; + sha256 = "09ac9dcp6cr57vjzyiy213y7312jqcy84mkamp99zr40qd1gwnyk"; + }; + + num-integer = pkgs.buildRustCrate { + pname = "num-integer"; + version = "0.1.44"; + crateName = "num-integer"; + dependencies = [ num-traits ]; + buildDependencies = [ autocfg ]; + sha256 = "1gdbnfgnivp90h644wmqj4a20yfmdga2xxxacx53pjbcazvfvajc"; + }; + + chrono = pkgs.buildRustCrate { + pname = "chrono"; + version = "0.4.19"; + crateName = "chrono"; + dependencies = [ num-traits num-integer ]; + features = [ "alloc" "std" ]; + sha256 = "0cjf5dnfbk99607vz6n5r6bhwykcypq5psihvk845sxrhnzadsar"; + }; + + imap-proto = pkgs.buildRustCrate { + pname = "imap-proto"; + version = "0.10.2"; + crateName = "imap-proto"; + dependencies = [ nom ]; + sha256 = "1bf5r4d0z7c8wxrvr7kjy26500wr7cd4sxz49ix3b3yzc6ayyqv1"; + }; + + lazy_static = pkgs.buildRustCrate { + pname = "lazy_static"; + version = "1.4.0"; + crateName = "lazy_static"; + sha256 = "13h6sdghdcy7vcqsm2gasfw3qg7ssa0fl3sw7lq6pdkbk52wbyfr"; + }; + + imap = pkgs.buildRustCrate { + pname = "imap"; + version = "2.4.0"; + crateName = "imap"; + edition = "2018"; + dependencies = [ + base64 + bufstream + chrono + imap-proto + lazy_static + nom + regex + ]; + sha256 = "1nj6x45qnid98nv637623rrh7imcxk0kad89ry8j5dkkgccvjyc0"; + }; + + epoll = pkgs.buildRustCrate { + pname = "epoll"; + version = "4.3.1"; + crateName = "epoll"; + dependencies = [ bitflags libc ]; + sha256 = "0dgmgdmrfbjkpxn1w3xmmwsm2a623a9qdwn90s8yl78n4a36kbh9"; + }; + } -- cgit 1.4.1