Compare commits
3 Commits
702a6ffb33
...
fetch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a4e9ae4bc | ||
|
|
002393b18e | ||
|
|
26cdccee3c |
12
README.md
12
README.md
@@ -6,7 +6,7 @@ Although this is quite minimal and only implements a tiny subset of the IMAP pro
|
|||||||
|
|
||||||
## Installation ##
|
## Installation ##
|
||||||
|
|
||||||
Get [rust] and run
|
Get [Rust] and run
|
||||||
|
|
||||||
cargo build --release
|
cargo build --release
|
||||||
|
|
||||||
@@ -33,13 +33,15 @@ The output of `imapidle --help` does a good job at explaining how to use it:
|
|||||||
|
|
||||||
Note that it only supports TLS encrypted IMAP and plain password authentication. Also, it currently reads the password from the command line, which isn't a great thing to do. I might change that eventually.
|
Note that it only supports TLS encrypted IMAP and plain password authentication. Also, it currently reads the password from the command line, which isn't a great thing to do. I might change that eventually.
|
||||||
|
|
||||||
## Why? ##
|
## Goals ##
|
||||||
|
|
||||||
Rust might not be the canonical programming language to do something like this in. And also, this probably already exists somewhere in a more complete and polished form. So this might not useful for anyone else. But I had three goals in making this:
|
I made this for the following reasons:
|
||||||
|
|
||||||
1. I wanted my emails to arrive faster and without having to manually hit the refresh button.
|
1. I wanted my emails to arrive faster and without having to manually hit the refresh button.
|
||||||
2. I wanted to find out how IMAP works and why it's often so slow (I'm still not sure about the latter).
|
2. I wanted to find out how IMAP works and why it's often so slow (I'm still not really sure).
|
||||||
3. I wanted to try using rust for something practical and see how well it works. The result is it worked, and I would do it again.
|
3. I wanted to try using Rust for something practical and see how well it works. It worked pretty well.
|
||||||
|
|
||||||
|
In terms of actual usability, this works fine for me, but I'm sure there are better alternatives out there.
|
||||||
|
|
||||||
[Rust]: https://www.rust-lang.org/
|
[Rust]: https://www.rust-lang.org/
|
||||||
[IMAP]: https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol
|
[IMAP]: https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol
|
||||||
|
|||||||
105
src/lib.rs
105
src/lib.rs
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
use rustls::{OwnedTrustAnchor, ClientConfig, RootCertStore, ClientConnection};
|
use rustls::{OwnedTrustAnchor, ClientConfig, RootCertStore, ClientConnection};
|
||||||
use anyhow::{Result as AResult, bail};
|
use anyhow::{Result as AResult, bail};
|
||||||
use std::net::{TcpStream, ToSocketAddrs};
|
use std::net::{TcpStream, ToSocketAddrs, SocketAddr};
|
||||||
use std::io::{self, ErrorKind, Read, Write, Error as IOError};
|
use std::io::{self, ErrorKind, Read, Write, Error as IOError};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::cell::RefCell;
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
@@ -22,6 +23,10 @@ pub struct Cli {
|
|||||||
#[arg(long, default_value_t = 993)]
|
#[arg(long, default_value_t = 993)]
|
||||||
port: u16,
|
port: u16,
|
||||||
|
|
||||||
|
// the resolved address(es)
|
||||||
|
#[arg(skip)]
|
||||||
|
addrs: RefCell<Vec<SocketAddr>>,
|
||||||
|
|
||||||
/// IMAP user name
|
/// IMAP user name
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
username: String,
|
username: String,
|
||||||
@@ -46,11 +51,12 @@ pub struct Cli {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Status {
|
struct Status {
|
||||||
connected: bool,
|
connected: bool,
|
||||||
last_run: SystemTime
|
last_run: SystemTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CONNECTION_LOST_ERRORS: &[ErrorKind] = &[
|
const CONNECTION_LOST_ERRORS: &[ErrorKind] = &[
|
||||||
ErrorKind::Interrupted,
|
ErrorKind::Interrupted,
|
||||||
|
ErrorKind::UnexpectedEOF, // server closes TLS connection without close_notify
|
||||||
ErrorKind::WouldBlock, // when a read times out
|
ErrorKind::WouldBlock, // when a read times out
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -152,7 +158,8 @@ pub fn run() -> AResult<()> {
|
|||||||
connection_status.lock().unwrap().connected = false;
|
connection_status.lock().unwrap().connected = false;
|
||||||
|
|
||||||
time_to_reconnect = 1;
|
time_to_reconnect = 1;
|
||||||
println!("Connection lost, reconnecting in {time_to_reconnect} seconds");
|
let error_kind = io_err.kind();
|
||||||
|
println!("Connection lost ({error_kind}), reconnecting in {time_to_reconnect} seconds");
|
||||||
thread::sleep(Duration::from_secs(time_to_reconnect));
|
thread::sleep(Duration::from_secs(time_to_reconnect));
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -162,7 +169,13 @@ pub fn run() -> AResult<()> {
|
|||||||
|
|
||||||
time_to_reconnect = u64::min(time_to_reconnect*2, 1800);
|
time_to_reconnect = u64::min(time_to_reconnect*2, 1800);
|
||||||
|
|
||||||
println!("Cannot connect currently, retrying in {time_to_reconnect} seconds");
|
if cli.verbose > 0 {
|
||||||
|
println!("Error: {:?}", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
let error_kind = io_err.kind();
|
||||||
|
println!("Cannot connect currently ({error_kind}), retrying in {time_to_reconnect} seconds");
|
||||||
|
|
||||||
thread::sleep(Duration::from_secs(time_to_reconnect));
|
thread::sleep(Duration::from_secs(time_to_reconnect));
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -203,10 +216,16 @@ pub fn connect_and_idle<F: Fn(), G: Fn()>(cli: &Cli, connected_callback: F, mail
|
|||||||
let mut tls_client = ClientConnection::new(
|
let mut tls_client = ClientConnection::new(
|
||||||
Arc::new(tls_config),
|
Arc::new(tls_config),
|
||||||
cli.server.as_str().try_into().unwrap())?;
|
cli.server.as_str().try_into().unwrap())?;
|
||||||
let addrs = (cli.server.as_str(), cli.port)
|
|
||||||
.to_socket_addrs()
|
let mut addrs = cli.addrs.borrow_mut();
|
||||||
.map_err(|e|io::Error::new(ErrorKind::NotConnected, e.to_string()))?
|
if addrs.is_empty() {
|
||||||
.collect::<Vec<_>>();
|
addrs.extend(
|
||||||
|
(cli.server.as_str(), cli.port)
|
||||||
|
.to_socket_addrs()
|
||||||
|
.map_err(|e|io::Error::new(ErrorKind::NotConnected, e.to_string()))?
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let mut socket = TcpStream::connect(addrs.as_slice())?;
|
let mut socket = TcpStream::connect(addrs.as_slice())?;
|
||||||
let mut state = ImapState::Unauthenticated;
|
let mut state = ImapState::Unauthenticated;
|
||||||
|
|
||||||
@@ -220,11 +239,20 @@ pub fn connect_and_idle<F: Fn(), G: Fn()>(cli: &Cli, connected_callback: F, mail
|
|||||||
} else if tls_client.wants_read() {
|
} else if tls_client.wants_read() {
|
||||||
let _i = tls_client.read_tls(&mut socket)?;
|
let _i = tls_client.read_tls(&mut socket)?;
|
||||||
|
|
||||||
if tls_client.process_new_packets()?.plaintext_bytes_to_read() == 0 {
|
let bytes_to_read = tls_client.process_new_packets()?.plaintext_bytes_to_read();
|
||||||
|
if bytes_to_read == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let len = tls_client.reader().read(&mut buffer)?;
|
println!("Bytes to read: {bytes_to_read}");
|
||||||
|
|
||||||
|
let mut len = tls_client.reader().read(&mut buffer)?;
|
||||||
|
|
||||||
|
while len == buffer.len() {
|
||||||
|
println!("Buffer full, read again");
|
||||||
|
len = tls_client.reader().read(&mut buffer)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
let responses = buffer[0..len]
|
let responses = buffer[0..len]
|
||||||
.split(|&x|x == b'\r' || x == b'\n')
|
.split(|&x|x == b'\r' || x == b'\n')
|
||||||
@@ -242,35 +270,44 @@ pub fn connect_and_idle<F: Fn(), G: Fn()>(cli: &Cli, connected_callback: F, mail
|
|||||||
}
|
}
|
||||||
|
|
||||||
match state {
|
match state {
|
||||||
ImapState::Unauthenticated => if response.starts_with(b"* OK") {
|
ImapState::Unauthenticated =>
|
||||||
let request = format!("A001 login {} {}\r\n", cli.username, cli.password);
|
if response.starts_with(b"* OK") {
|
||||||
tls_client.writer().write(request.as_bytes())?;
|
let request = format!("A001 login {} {}\r\n", cli.username, cli.password);
|
||||||
state = ImapState::Authenticated;
|
tls_client.writer().write(request.as_bytes())?;
|
||||||
},
|
state = ImapState::Authenticated;
|
||||||
ImapState::Authenticated => if response.starts_with(b"A001 OK") {
|
},
|
||||||
tls_client.writer().write(b"A002 select inbox\r\n")?;
|
ImapState::Authenticated =>
|
||||||
state = ImapState::Inbox;
|
if response.starts_with(b"A001 OK") {
|
||||||
} else if response.starts_with(b"A001") {
|
tls_client.writer().write(b"A002 select inbox\r\n")?;
|
||||||
bail!("The server rejected authentication");
|
state = ImapState::Authenticated;
|
||||||
},
|
} else if response.starts_with(b"A002 OK") {
|
||||||
ImapState::Inbox => if response.starts_with(b"A002 OK") {
|
tls_client.writer().write(b"A003 fetch 1:* all\r\n")?;
|
||||||
tls_client.writer().write(b"A003 idle\r\n")?;
|
state = ImapState::Inbox;
|
||||||
state = ImapState::Idling;
|
} else if response.starts_with(b"A001") || response.starts_with(b"A002") {
|
||||||
connected_callback();
|
bail!("The server rejected authentication");
|
||||||
// notify timer thread that we're live
|
},
|
||||||
} else if response.starts_with(b"A002") {
|
ImapState::Inbox =>
|
||||||
bail!("Selecting inbox failed");
|
if response.starts_with(b"A003 OK") {
|
||||||
},
|
tls_client.writer().write(b"A004 idle\r\n")?;
|
||||||
ImapState::Idling => if response.starts_with(b"+ idling") {
|
state = ImapState::Idling;
|
||||||
println!("Connected and idling ...");
|
connected_callback(); // notify timer thread that we're live
|
||||||
} else if response.starts_with(b"*") && response.ends_with(b"EXISTS") {
|
} else if response.starts_with(b"A002") {
|
||||||
mail_callback();
|
bail!("Selecting inbox failed");
|
||||||
}
|
},
|
||||||
|
ImapState::Idling =>
|
||||||
|
if response.starts_with(b"+ idling") {
|
||||||
|
println!("Connected and idling ...");
|
||||||
|
} else if response.starts_with(b"*") && response.ends_with(b"EXISTS") {
|
||||||
|
mail_callback();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// if wants_read() and wants_write() are both false, this usually means the connection was closed
|
// if wants_read() and wants_write() are both false, this usually means the connection was closed
|
||||||
// so just return "Interrupted" and reconnect after a few seconds
|
// so just return "Interrupted" and reconnect after a few seconds
|
||||||
|
if cli.verbose > 0 {
|
||||||
|
println!("wants_read() and wants_write() are both false");
|
||||||
|
}
|
||||||
return Err(io::Error::new(ErrorKind::Interrupted, "Connection was closed by server").into());
|
return Err(io::Error::new(ErrorKind::Interrupted, "Connection was closed by server").into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user