Add (crude) initial prototype to fetch new emails
parent
a5573eb320
commit
2da2859e9b
File diff suppressed because it is too large
Load Diff
10
Cargo.toml
10
Cargo.toml
|
@ -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"
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
45
src/lib.rs
45
src/lib.rs
|
@ -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
|
||||
}
|
||||
|
|
18
src/main.rs
18
src/main.rs
|
@ -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");
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue