Add (crude) initial prototype to fetch new emails

master
Nick Krichevsky 2022-05-08 14:01:16 -04:00
parent a5573eb320
commit 2da2859e9b
5 changed files with 1433 additions and 1 deletions

1260
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,16 @@ edition = "2021"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_yaml = "0.8.24"
derive-getters = "0.2.0"
async-imap = "0.5.0"
mailparse = "0.13.8"
tokio = { version = "1.18", features = ["full"] }
# For annoying reasons, we must pin exactly the same versions as async-imap if we want to use
# their types.
# https://github.com/async-email/async-imap/pull/57
futures = "0.3.15"
async-native-tls = { version = "0.3.3" }
async-std = { version = "1.8.0", default-features = false, features = ["std"] }
[dev-dependencies]
textwrap = "0.15.0"

101
src/fetch.rs Normal file
View File

@ -0,0 +1,101 @@
use std::{fmt::Debug, time::Duration};
use async_imap::{
extensions::idle::{Handle, IdleResponse},
imap_proto::{MailboxDatum, Response},
Session,
};
use futures::{AsyncRead, AsyncWrite, StreamExt};
const WAIT_TIMEOUT: Duration = Duration::from_secs(29 * 60);
mod idle {
use super::{IdleResponse, Response};
/// Data is a type-safe wrapper for [`IdleResponse`]. This acts as a wrapper type so
/// we can extract the response data from a response (the data stored in this variant has a private type).
pub struct Data(IdleResponse);
impl Data {
/// `new` constructs a new `IdleData` from an [`IdleResponse`] containing data.
///
/// # Panics
/// Will panic if the response does not have the variant of `IdleResponse::newData`. This is a private module,
/// where we should control the data going in, so we really do consider this unrecoverable.
pub fn new(response: IdleResponse) -> Self {
assert!(matches!(response, IdleResponse::NewData(_)));
Self(response)
}
/// `response` gets the server's response out of our `Data`.
///
/// # Panics
/// This can panic if `Data`'s type storage invariant is violted.
pub fn response(&self) -> &Response {
match &self.0 {
IdleResponse::NewData(data) => data.parsed(),
_ => panic!("not possible by construction"),
}
}
}
}
/// `fetch_email` will consume the current session and put it into an `Idle` state, until a new email is received
///
/// # Errors
/// If, for any reason, the email fails to be fetched, one of `async_imap` error's will be returned.
#[allow(clippy::module_name_repetitions)]
pub async fn fetch_email<T>(session: Session<T>) -> async_imap::error::Result<Session<T>>
where
T: AsyncRead + AsyncWrite + Unpin + Debug + Send,
{
// TODO: check if the server can handle IDLE
let mut idle_handle = session.idle();
idle_handle.init().await?;
let response_data = idle_until_data_received(&mut idle_handle).await?;
let response = response_data.response();
let sequence_number = match response {
Response::MailboxData(MailboxDatum::Exists(seq)) => seq,
_ => panic!("no idea what to do with this {:?}", response),
};
let mut unidled_session = idle_handle.done().await?;
// TODO: This should be done somewhere other than this function, in some kind of concurrent task.
let message = unidled_session
.fetch(format!("{:?}", sequence_number), "RFC822")
.await?
// god please don't do this just to get a single message
.next()
.await
.unwrap()?;
let parsed = mailparse::parse_mail(message.body().unwrap()).unwrap();
dbg!(parsed.get_body().unwrap());
let subparts = parsed.subparts;
let res = subparts
.into_iter()
.map(|x| x.get_body())
.collect::<Result<String, _>>();
println!("{}", res.unwrap());
Ok(unidled_session)
}
async fn idle_until_data_received<T>(
idle_handle: &mut Handle<T>,
) -> async_imap::error::Result<idle::Data>
where
T: AsyncRead + AsyncWrite + Unpin + Debug + Send,
{
loop {
let (idle_response_future, _stop) = idle_handle.wait_with_timeout(WAIT_TIMEOUT);
let idle_response = idle_response_future.await?;
match idle_response {
IdleResponse::ManualInterrupt => panic!("we don't interrupt manually"),
IdleResponse::Timeout => continue,
IdleResponse::NewData(_) => return Ok(idle::Data::new(idle_response)),
}
}
}

View File

@ -1,4 +1,49 @@
#![warn(clippy::all, clippy::pedantic)]
// TODO: Remove the need for these. I'm experimenting right now.
#![allow(clippy::missing_errors_doc)]
use std::fmt::Debug;
use async_imap::{self, Client, Session};
use async_native_tls::TlsConnector;
pub use config::{Config, IMAP as IMAPConfig};
use futures::{AsyncRead, AsyncWrite};
mod config;
mod fetch;
pub async fn setup_session(
cfg: &Config,
) -> async_imap::error::Result<Session<impl AsyncRead + AsyncWrite + Unpin + Debug + Send>> {
let imap_cfg = cfg.imap();
println!("Logging in...");
let client = build_imap_client(imap_cfg).await?;
client
.login(imap_cfg.username(), imap_cfg.password())
.await
.map_err(|err| err.0)
}
pub async fn fetch_emails<T>(session: Session<T>) -> async_imap::error::Result<()>
where
T: AsyncRead + AsyncWrite + Unpin + Debug + Send,
{
let mut current_session = session;
current_session.examine("INBOX").await?;
loop {
println!("Idling...");
current_session = fetch::fetch_email(current_session).await?;
}
}
async fn build_imap_client(
imap_cfg: &IMAPConfig,
) -> async_imap::error::Result<Client<impl AsyncRead + AsyncWrite + Unpin + Debug>> {
let tls_connector = TlsConnector::new();
async_imap::connect(
(imap_cfg.domain(), imap_cfg.port()),
imap_cfg.domain(),
tls_connector,
)
.await
}

View File

@ -1,3 +1,19 @@
use std::fs::File;
use tokio::runtime::Runtime;
use ynabifier::Config;
fn main() {
println!("Hello, world!");
let config_file = File::open("config.yml").expect("failed to open config file");
let config =
serde_yaml::from_reader::<_, Config>(config_file).expect("failed to parse config file");
let runtime = Runtime::new().expect("failed to create runtime");
runtime.block_on(async {
let session = ynabifier::setup_session(&config)
.await
.expect("failed to setup socket");
ynabifier::fetch_emails(session)
.await
.expect("failed to fetch emails");
});
}