This commit is contained in:
Michael Zhang 2021-03-08 17:07:44 -06:00
parent 7153b30a43
commit 7cd69bd6a8
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
15 changed files with 169 additions and 53 deletions

13
Cargo.lock generated
View file

@ -212,7 +212,7 @@ dependencies = [
"git2",
"git2-curl",
"glob",
"hex 0.4.2",
"hex 0.4.3",
"home",
"humantime",
"ignore",
@ -949,9 +949,9 @@ checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77"
[[package]]
name = "hex"
version = "0.4.2"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
@ -1425,6 +1425,7 @@ dependencies = [
"anyhow",
"assert_matches",
"async-trait",
"bytes",
"derive_builder",
"futures",
"log",
@ -1434,6 +1435,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tokio-stream",
"tokio-util",
"webpki-roots",
]
@ -1481,15 +1483,14 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pest"
version = "2.1.3"
source = "git+https://github.com/iptq/pest?rev=838adc941cc182e2c2d807e245a1e4c3e8594036#838adc941cc182e2c2d807e245a1e4c3e8594036"
dependencies = [
"log",
"ucd-trie",
]
[[package]]
name = "pest_derive"
version = "2.1.0"
source = "git+https://github.com/iptq/pest?rev=838adc941cc182e2c2d807e245a1e4c3e8594036#838adc941cc182e2c2d807e245a1e4c3e8594036"
dependencies = [
"pest",
"pest_generator",
@ -1498,7 +1499,6 @@ dependencies = [
[[package]]
name = "pest_generator"
version = "2.1.3"
source = "git+https://github.com/iptq/pest?rev=838adc941cc182e2c2d807e245a1e4c3e8594036#838adc941cc182e2c2d807e245a1e4c3e8594036"
dependencies = [
"pest",
"pest_meta",
@ -1510,7 +1510,6 @@ dependencies = [
[[package]]
name = "pest_meta"
version = "2.1.3"
source = "git+https://github.com/iptq/pest?rev=838adc941cc182e2c2d807e245a1e4c3e8594036#838adc941cc182e2c2d807e245a1e4c3e8594036"
dependencies = [
"cargo",
"maplit",

View file

@ -14,15 +14,19 @@ maintenance = { status = "passively-maintained" }
[dependencies]
anyhow = "1.0.38"
async-trait = "0.1.42"
bytes = { version = "1.0.1" }
derive_builder = "0.9.0"
futures = "0.3.12"
log = "0.4.14"
parking_lot = "0.11.1"
pest = { git = "https://github.com/iptq/pest", rev = "838adc941cc182e2c2d807e245a1e4c3e8594036" }
pest_derive = { git = "https://github.com/iptq/pest", rev = "838adc941cc182e2c2d807e245a1e4c3e8594036" }
# pest = { path = "../../pest/pest" }
# pest_derive = { path = "../../pest/derive" }
pest = { git = "https://github.com/iptq/pest", rev = "6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99" }
pest_derive = { git = "https://github.com/iptq/pest", rev = "6a4d3a3d10e42a3ee605ca979d0fcdac97a83a99" }
tokio = { version = "1.1.1", features = ["full"] }
tokio-rustls = "0.22.0"
tokio-stream = "0.1.3"
tokio-util = { version = "0.6.3" }
webpki-roots = "0.21.0"
[dev-dependencies]

View file

@ -4,10 +4,9 @@ use std::task::{Context, Poll};
use anyhow::Result;
use futures::{
future::{self, Either, FutureExt, TryFutureExt},
stream::{Peekable, Stream, StreamExt},
future::{self, FutureExt, TryFutureExt},
stream::{Stream, StreamExt},
};
use parking_lot::Mutex;
use tokio::{
io::{
self, AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader, ReadHalf, WriteHalf,
@ -21,8 +20,9 @@ use tokio::{
use tokio_rustls::{
client::TlsStream, rustls::ClientConfig as RustlsConfig, webpki::DNSNameRef, TlsConnector,
};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tokio_util::codec::FramedRead;
use crate::codec::ImapCodec;
use crate::command::Command;
use crate::parser::{parse_capability, parse_response};
use crate::response::{Response, ResponseDone};
@ -189,16 +189,20 @@ async fn listen<C>(
where
C: AsyncRead + Unpin,
{
let mut reader = BufReader::new(conn);
let codec = ImapCodec::default();
let mut framed = FramedRead::new(conn, codec);
// let mut reader = BufReader::new(conn);
let mut greeting_tx = Some(greeting_tx);
let mut curr_cmd: Option<Command2> = None;
let mut exit_rx = exit_rx.map_err(|_| ()).shared();
// let mut exit_fut = Some(exit_rx.fuse());
// let mut fut1 = None;
let mut cache = String::new();
loop {
let mut next_line = String::new();
let read_fut = reader.read_line(&mut next_line).fuse();
// let mut next_line = String::new();
// let read_fut = reader.read_line(&mut next_line).fuse();
let read_fut = framed.next().fuse();
pin_mut!(read_fut);
// only listen for a new command if there isn't one already
@ -222,10 +226,24 @@ where
}
}
len = read_fut => {
trace!("read line {:?}", next_line);
resp = read_fut => {
// trace!("read line {:?}", next_line);
// res should not be None here
let resp = parse_response(next_line)?;
// cache += &next_line;
// let resp = match parse_response(&cache) {
// Ok(v) => {
// cache.clear();
// v
// }
// Err(e) => {
// error!("parse error: {}", e);
// continue;
// }
// };
let resp = match resp {
Some(Ok(v)) => v,
a => { error!("failed: {:?}", a); bail!("fuck"); },
};
// if this is the very first response, then it's a greeting
if let Some(greeting_tx) = greeting_tx.take() {
@ -244,6 +262,6 @@ where
}
}
let conn = reader.into_inner();
let conn = framed.into_inner();
Ok(conn)
}

View file

@ -43,10 +43,9 @@ use tokio::net::TcpStream;
use tokio_rustls::{
client::TlsStream, rustls::ClientConfig as RustlsConfig, webpki::DNSNameRef, TlsConnector,
};
use tokio_stream::wrappers::UnboundedReceiverStream;
use crate::command::{Command, FetchItems, SearchCriteria};
use crate::response::{MailboxData, Response, ResponseData, ResponseDone};
use crate::response::{AttributeValue, MailboxData, Response, ResponseData, ResponseDone};
pub use self::inner::{Client, ResponseStream};
@ -200,7 +199,7 @@ impl ClientAuthenticated {
}
/// Runs the UID FETCH command
pub async fn uid_fetch(&mut self, uids: &[u32]) -> Result<()> {
pub async fn uid_fetch(&mut self, uids: &[u32]) -> Result<Vec<(u32, Vec<AttributeValue>)>> {
let cmd = Command::UidFetch {
uids: uids.to_vec(),
items: FetchItems::All,
@ -208,11 +207,13 @@ impl ClientAuthenticated {
debug!("uid fetch: {}", cmd);
let stream = self.execute(cmd).await?;
let (done, data) = stream.wait().await?;
debug!("done: {:?} {:?}", done, data);
for resp in data {
debug!("uid fetch: {:?}", resp);
}
todo!()
Ok(data
.into_iter()
.filter_map(|resp| match resp {
Response::Fetch(n, attrs) => Some((n, attrs)),
_ => None,
})
.collect())
}
/// Runs the IDLE command

30
imap/src/codec.rs Normal file
View file

@ -0,0 +1,30 @@
use bytes::{Buf, BytesMut};
use tokio_util::codec::Decoder;
use crate::parser::parse_streamed_response;
use crate::response::Response;
#[derive(Default)]
pub struct ImapCodec;
impl Decoder for ImapCodec {
type Item = Response;
type Error = anyhow::Error;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
let s = std::str::from_utf8(src)?;
debug!("-------------------------");
debug!("s: {:?}", s);
match parse_streamed_response(s) {
Ok((resp, len)) => {
src.advance(len);
return Ok(Some(resp));
}
Err(e) => {
warn!("failed to parse: {:?} {}", e, e);
}
};
Ok(None)
}
}

View file

@ -20,6 +20,7 @@ extern crate log;
extern crate pest_derive;
pub mod client;
pub mod codec;
pub mod command;
pub mod parser;
pub mod response;

View file

@ -1,31 +1,27 @@
use pest::{ParseResult as PestResult, ParserState};
use pest::{iterators::QueueableToken, ParseResult as PestResult, ParserState};
use super::Rule;
type PSR<'a> = Box<ParserState<'a, Rule>>;
/// This is a hack around the literal syntax to allow us to parse characters statefully.
pub(crate) fn literal_internal(state: PSR) -> PestResult<PSR> {
pub(crate) fn literal_internal(mut state: PSR) -> PestResult<PSR> {
// debug!("STATE: {:?}", state);
use pest::Atomicity;
// yoinked from the generated code
#[inline]
#[allow(non_snake_case, unused_variables)]
pub fn digit(state: PSR) -> PestResult<PSR> {
state.rule(Rule::digit, |state| {
state.atomic(Atomicity::Atomic, |state| {
state.match_range('\u{30}'..'\u{39}')
})
})
state.match_range('\u{30}'..'\u{39}')
}
#[inline]
#[allow(non_snake_case, unused_variables)]
pub fn number(state: PSR) -> PestResult<PSR> {
state.rule(Rule::number, |state| {
state.atomic(Atomicity::Atomic, |state| {
state.sequence(|state| {
digit(state).and_then(|state| state.repeat(|state| digit(state)))
})
state.sequence(|state| {
debug!("number::atomic::sequence");
digit(state).and_then(|state| state.repeat(digit))
})
})
}
@ -41,18 +37,49 @@ pub(crate) fn literal_internal(state: PSR) -> PestResult<PSR> {
#[inline]
#[allow(non_snake_case, unused_variables)]
pub fn crlf(state: PSR) -> PestResult<PSR> {
debug!(
"running rule 'crlf' {:?}",
state.queue().iter().rev().take(10).collect::<Vec<_>>()
);
state.sequence(|state| state.match_string("\r")?.match_string("\n"))
}
let state = state.match_string("{").and_then(number)?;
let state: PSR = state.match_string("{").and_then(number)?;
let num_chars = {
let mut queue = state.queue().iter().rev();
println!("QUEUE: {:?}", queue);
let end = queue.next().unwrap();
let start = queue.next().unwrap();
let queue = state.queue();
let (start_idx, end_pos) = queue
.iter()
.rev()
.find_map(|p| match p {
QueueableToken::End {
start_token_index: start,
rule: Rule::number,
input_pos: pos,
} => Some((*start, *pos)),
_ => None,
})
.unwrap();
let start_pos = match queue[start_idx] {
QueueableToken::Start { input_pos: pos, .. } => pos,
_ => unreachable!(),
};
debug!("start_pos: {}, end_pos: {}", start_pos, end_pos);
let inp = state.position().get_str();
let seg = &inp[start.input_pos()..end.input_pos()];
seg.parse::<usize>().unwrap()
let seg = &inp[start_pos..end_pos];
match seg.parse::<usize>() {
Ok(v) => {
debug!("got length: {}", v);
v
}
Err(e) => {
error!(
"failed to parse int from {}..{} {:?}: {}",
start_pos, end_pos, seg, e
);
return Err(state);
}
}
};
state

View file

@ -25,6 +25,15 @@ pub fn parse_capability(s: impl AsRef<str>) -> ParseResult<Capability> {
Ok(build_capability(pair))
}
pub fn parse_streamed_response(s: impl AsRef<str>) -> ParseResult<(Response, usize)> {
let mut pairs = Rfc3501::parse(Rule::streamed_response, s.as_ref())?;
let pair = unwrap1(pairs.next().unwrap());
let span = pair.as_span();
let range = span.end() - span.start();
let response = build_response(pair);
Ok((response, range))
}
pub fn parse_response(s: impl AsRef<str>) -> ParseResult<Response> {
let mut pairs = Rfc3501::parse(Rule::response, s.as_ref())?;
let pair = pairs.next().unwrap();
@ -386,6 +395,7 @@ fn build_nstring(pair: Pair<Rule>) -> Option<String> {
if matches!(pair.as_rule(), Rule::nil) {
return None;
}
Some(build_string(pair))
}

View file

@ -1,3 +1,5 @@
streamed_response = { response ~ ANY* }
// formal syntax from https://tools.ietf.org/html/rfc3501#section-9
addr_adl = { nstring }
addr_host = { nstring }
@ -93,7 +95,7 @@ msg_att_static_internaldate = { ^"INTERNALDATE" ~ sp ~ date_time }
msg_att_static_rfc822_size = { ^"RFC822.SIZE" ~ sp ~ number }
msg_att_static_uid = { ^"UID" ~ sp ~ uniqueid }
nil = { ^"NIL" }
nstring = { string | nil }
nstring = { nil | string }
number = @{ digit{1,} }
nz_number = @{ digit_nz ~ digit* }
quoted = @{ dquote ~ quoted_char* ~ dquote }
@ -124,7 +126,7 @@ section_spec = { section_msgtext | (section_part ~ ("." ~ section_text)?) }
section_text = { section_msgtext | "MIME" }
status_att = { ^"MESSAGES" | ^"RECENT" | ^"UIDNEXT" | ^"UIDVALIDITY" | ^"UNSEEN" }
status_att_list = { status_att ~ sp ~ number ~ (sp ~ status_att ~ sp ~ number)* }
string = @{ quoted | literal }
string = ${ quoted | literal }
tag = @{ tag_char{1,} }
tag_char = @{ !"+" ~ astring_char }
text = @{ text_char{1,} }

View file

@ -35,6 +35,9 @@ pub enum MailCommand {
pub enum MailEvent {
/// Got the list of folders
FolderList(Vec<String>),
/// Got the current list of messages
MessageList(Vec<String>),
}
/// Main entrypoint for the mail listener.
@ -139,8 +142,13 @@ async fn imap_main(acct: MailAccountConfig, mail2ui_tx: UnboundedSender<MailEven
debug!("mailbox list: {:?}", folder_list);
let _ = mail2ui_tx.send(MailEvent::FolderList(folder_list));
let message_list = authed.uid_search().await?;
authed.uid_fetch(&message_list).await?;
let message_uids = authed.uid_search().await?;
let message_list = authed.uid_fetch(&message_uids).await?;
let mut messages = Vec::new();
for (_, attrs) in message_list {
messages.push(format!("{:?}", attrs));
}
let _ = mail2ui_tx.send(MailEvent::MessageList(messages));
let mut idle_stream = authed.idle().await?;

View file

@ -3,7 +3,7 @@ use std::thread;
use anyhow::Result;
use fern::colors::{Color, ColoredLevelConfig};
use futures::future::TryFutureExt;
use futures::future::{FutureExt, TryFutureExt};
use panorama::{
config::spawn_config_watcher_system,
mail::{self, MailEvent},
@ -109,6 +109,7 @@ fn setup_logger(log_file: Option<impl AsRef<Path>>) -> Result<()> {
.warn(Color::Yellow)
.error(Color::Red);
let mut logger = fern::Dispatch::new()
.filter(|meta| meta.target() != "tokio_util::codec::framed_impl")
.format(move |out, message, record| {
out.finish(format_args!(
"{}[{}][{}] {}",

View file

@ -2,12 +2,13 @@ use tui::{
buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Span, Spans},
widgets::*,
};
use super::FrameType;
pub fn render_mail_tab(f: &mut FrameType, area: Rect, folders: &[String]) {
pub fn render_mail_tab(f: &mut FrameType, area: Rect, folders: &[String], messages: &[String]) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.margin(0)
@ -25,5 +26,15 @@ pub fn render_mail_tab(f: &mut FrameType, area: Rect, folders: &[String]) {
.highlight_style(Style::default().add_modifier(Modifier::ITALIC))
.highlight_symbol(">>");
let rows = messages
.iter()
.map(|s| Row::new(vec![s.as_ref()]))
.collect::<Vec<_>>();
let table = Table::new(rows)
.style(Style::default().fg(Color::White))
.widths(&[Constraint::Max(5000)])
.highlight_style(Style::default().add_modifier(Modifier::BOLD));
f.render_widget(dirlist, chunks[0]);
f.render_widget(table, chunks[1]);
}

View file

@ -44,6 +44,7 @@ pub async fn run_ui(
let mut term = Terminal::new(backend)?;
let mut folders = Vec::<String>::new();
let mut messages = Vec::<String>::new();
loop {
term.draw(|f| {
@ -58,7 +59,7 @@ pub async fn run_ui(
let tabs = Tabs::new(titles);
f.render_widget(tabs, chunks[0]);
render_mail_tab(f, chunks[1], &folders);
render_mail_tab(f, chunks[1], &folders, &messages);
})?;
let event = if event::poll(FRAME_DURATION)? {
@ -88,6 +89,9 @@ pub async fn run_ui(
MailEvent::FolderList(new_folders) => {
folders = new_folders;
}
MailEvent::MessageList(new_messages) => {
messages = new_messages;
}
}
}