start merging email with msg, add list msgs command

This commit is contained in:
Clément DOUIN 2021-01-16 13:16:40 +01:00
parent a803800d1c
commit 0a508f2e95
No known key found for this signature in database
GPG key ID: 69C9B9CFFDEE2DEF
5 changed files with 235 additions and 84 deletions

View file

@ -4,7 +4,8 @@ use std::{fmt, net::TcpStream, result};
use crate::config; use crate::config;
use crate::email::{self, Email}; use crate::email::{self, Email};
use crate::mailbox::Mailbox; use crate::mbox::Mbox;
use crate::msg::Msg;
// Error wrapper // Error wrapper
@ -79,17 +80,40 @@ impl<'a> ImapConnector<'a> {
Ok(Self { config, sess }) Ok(Self { config, sess })
} }
pub fn list_mboxes(&mut self) -> Result<Vec<Mailbox<'_>>> { pub fn close(&mut self) {
match self.sess.close() {
_ => (),
}
}
pub fn list_mboxes(&mut self) -> Result<Vec<Mbox<'_>>> {
let mboxes = self let mboxes = self
.sess .sess
.list(Some(""), Some("*"))? .list(Some(""), Some("*"))?
.iter() .iter()
.map(Mailbox::from_name) .map(Mbox::from_name)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(mboxes) Ok(mboxes)
} }
pub fn list_msgs(&mut self, mbox: &str, page_size: &u32, page: &u32) -> Result<Vec<Msg>> {
let last_seq = self.sess.select(mbox)?.exists;
let begin = last_seq - (page * page_size);
let end = begin - (page_size - 1);
let range = format!("{}:{}", begin, end);
let msgs = self
.sess
.fetch(range, "(UID BODY.PEEK[])")?
.iter()
.rev()
.map(Msg::from)
.collect::<Vec<_>>();
Ok(msgs)
}
pub fn read_emails(&mut self, mbox: &str, query: &str) -> Result<Vec<Email<'_>>> { pub fn read_emails(&mut self, mbox: &str, query: &str) -> Result<Vec<Email<'_>>> {
self.sess.select(mbox)?; self.sess.select(mbox)?;

View file

@ -2,7 +2,7 @@ mod config;
mod email; mod email;
mod imap; mod imap;
mod input; mod input;
mod mailbox; mod mbox;
mod msg; mod msg;
mod smtp; mod smtp;
mod table; mod table;
@ -15,6 +15,9 @@ use crate::imap::ImapConnector;
use crate::msg::Msg; use crate::msg::Msg;
use crate::table::DisplayTable; use crate::table::DisplayTable;
const DEFAULT_PAGE_SIZE: u32 = 10;
const DEFAULT_PAGE: u32 = 0;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
ConfigError(config::Error), ConfigError(config::Error),
@ -89,14 +92,44 @@ fn uid_arg() -> Arg<'static, 'static> {
} }
fn run() -> Result<()> { fn run() -> Result<()> {
let default_page_size = &DEFAULT_PAGE_SIZE.to_string();
let default_page = &DEFAULT_PAGE.to_string();
let matches = App::new("Himalaya") let matches = App::new("Himalaya")
.version("0.1.0") .version("0.1.0")
.about("📫 Minimalist CLI email client") .about("📫 Minimalist CLI email client")
.author("soywod <clement.douin@posteo.net>") .author("soywod <clement.douin@posteo.net>")
.setting(AppSettings::ArgRequiredElseHelp) .setting(AppSettings::ArgRequiredElseHelp)
.subcommand(SubCommand::with_name("list").about("Lists all available mailboxes")) .subcommand(
SubCommand::with_name("mailboxes")
.aliases(&["mboxes", "mb", "m"])
.about("Lists all available mailboxes"),
)
.subcommand(
SubCommand::with_name("list")
.aliases(&["lst", "l"])
.about("Lists emails sorted by arrival date")
.arg(mailbox_arg())
.arg(
Arg::with_name("size")
.help("Page size")
.short("s")
.long("size")
.value_name("INT")
.default_value(default_page_size),
)
.arg(
Arg::with_name("page")
.help("Page number")
.short("p")
.long("page")
.value_name("INT")
.default_value(default_page),
),
)
.subcommand( .subcommand(
SubCommand::with_name("search") SubCommand::with_name("search")
.aliases(&["query", "q", "s"])
.about("Lists emails matching the given IMAP query") .about("Lists emails matching the given IMAP query")
.arg(mailbox_arg()) .arg(mailbox_arg())
.arg( .arg(
@ -109,6 +142,7 @@ fn run() -> Result<()> {
) )
.subcommand( .subcommand(
SubCommand::with_name("read") SubCommand::with_name("read")
.aliases(&["r"])
.about("Reads text bodies of an email") .about("Reads text bodies of an email")
.arg(uid_arg()) .arg(uid_arg())
.arg(mailbox_arg()) .arg(mailbox_arg())
@ -124,6 +158,7 @@ fn run() -> Result<()> {
) )
.subcommand( .subcommand(
SubCommand::with_name("attachments") SubCommand::with_name("attachments")
.aliases(&["attach", "a"])
.about("Downloads all attachments from an email") .about("Downloads all attachments from an email")
.arg(uid_arg()) .arg(uid_arg())
.arg(mailbox_arg()), .arg(mailbox_arg()),
@ -131,6 +166,7 @@ fn run() -> Result<()> {
.subcommand(SubCommand::with_name("write").about("Writes a new email")) .subcommand(SubCommand::with_name("write").about("Writes a new email"))
.subcommand( .subcommand(
SubCommand::with_name("reply") SubCommand::with_name("reply")
.aliases(&["rep", "re"])
.about("Answers to an email") .about("Answers to an email")
.arg(uid_arg()) .arg(uid_arg())
.arg(mailbox_arg()) .arg(mailbox_arg())
@ -143,94 +179,127 @@ fn run() -> Result<()> {
) )
.subcommand( .subcommand(
SubCommand::with_name("forward") SubCommand::with_name("forward")
.aliases(&["fwd", "f"])
.about("Forwards an email") .about("Forwards an email")
.arg(uid_arg()) .arg(uid_arg())
.arg(mailbox_arg()), .arg(mailbox_arg()),
) )
.get_matches(); .get_matches();
if let Some(_) = matches.subcommand_matches("list") { if let Some(_) = matches.subcommand_matches("mailboxes") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mboxes = ImapConnector::new(&config.imap)?.list_mboxes()?.to_table(); let mut imap_conn = ImapConnector::new(&config.imap)?;
println!("{}", mboxes); let mboxes = imap_conn.list_mboxes()?;
println!("{}", mboxes.to_table());
imap_conn.close();
}
if let Some(matches) = matches.subcommand_matches("list") {
let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?;
let mbox = matches.value_of("mailbox").unwrap();
let page_size: u32 = matches
.value_of("size")
.unwrap()
.parse()
.unwrap_or(DEFAULT_PAGE_SIZE);
let page: u32 = matches
.value_of("page")
.unwrap()
.parse()
.unwrap_or(DEFAULT_PAGE);
let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?;
println!("{}", msgs.to_table());
imap_conn.close();
} }
if let Some(matches) = matches.subcommand_matches("search") { if let Some(matches) = matches.subcommand_matches("search") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?;
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
let query = matches
if let Some(matches) = matches.values_of("query") { .values_of("query")
let query = matches .unwrap_or_default()
.fold((false, vec![]), |(escape, mut cmds), cmd| { .fold((false, vec![]), |(escape, mut cmds), cmd| {
match (cmd, escape) { match (cmd, escape) {
// Next command is an arg and needs to be escaped // Next command is an arg and needs to be escaped
("subject", _) | ("body", _) | ("text", _) => { ("subject", _) | ("body", _) | ("text", _) => {
cmds.push(cmd.to_string()); cmds.push(cmd.to_string());
(true, cmds) (true, cmds)
}
// Escaped arg commands
(_, true) => {
cmds.push(format!("\"{}\"", cmd));
(false, cmds)
}
// Regular commands
(_, false) => {
cmds.push(cmd.to_string());
(false, cmds)
}
} }
}) // Escaped arg commands
.1 (_, true) => {
.join(" "); cmds.push(format!("\"{}\"", cmd));
(false, cmds)
}
// Regular commands
(_, false) => {
cmds.push(cmd.to_string());
(false, cmds)
}
}
})
.1
.join(" ");
let emails = ImapConnector::new(&config.imap)? let msgs = imap_conn.read_emails(&mbox, &query)?;
.read_emails(&mbox, &query)? println!("{}", msgs.to_table());
.to_table();
println!("{}", emails); imap_conn.close();
}
} }
if let Some(matches) = matches.subcommand_matches("read") { if let Some(matches) = matches.subcommand_matches("read") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?;
let mbox = matches.value_of("mailbox").unwrap(); let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap(); let uid = matches.value_of("uid").unwrap();
let mime = format!("text/{}", matches.value_of("mime-type").unwrap()); let mime = format!("text/{}", matches.value_of("mime-type").unwrap());
let body = ImapConnector::new(&config.imap)?.read_email_body(&mbox, &uid, &mime)?;
let body = imap_conn.read_email_body(&mbox, &uid, &mime)?;
println!("{}", body); println!("{}", body);
imap_conn.close();
} }
if let Some(matches) = matches.subcommand_matches("attachments") { if let Some(matches) = matches.subcommand_matches("attachments") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let mut imap_conn = ImapConnector::new(&config.imap)?; let mut imap_conn = ImapConnector::new(&config.imap)?;
let msg = imap_conn.read_msg(&mbox, &uid)?; let mbox = matches.value_of("mailbox").unwrap();
let msg = Msg::from(&msg)?; let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice());
let parts = msg.extract_parts()?; let parts = msg.extract_parts()?;
if parts.is_empty() { if parts.is_empty() {
println!("No attachment found for message {}", uid); println!("No attachment found for message {}", uid);
} else { } else {
println!("{} attachment(s) found for message {}", parts.len(), uid); println!("{} attachment(s) found for message {}", parts.len(), uid);
msg.extract_parts()?.iter().for_each(|(filename, bytes)| { parts.iter().for_each(|(filename, bytes)| {
let filepath = config.downloads_filepath(&filename); let filepath = config.downloads_filepath(&filename);
println!("Downloading {}", filename); println!("Downloading {}", filename);
fs::write(filepath, bytes).unwrap() fs::write(filepath, bytes).unwrap()
}); });
println!("Done!"); println!("Done!");
} }
imap_conn.close();
} }
if let Some(_) = matches.subcommand_matches("write") { if let Some(_) = matches.subcommand_matches("write") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mut imap_conn = ImapConnector::new(&config.imap)?; let mut imap_conn = ImapConnector::new(&config.imap)?;
let tpl = Msg::build_new_tpl(&config)?; let tpl = Msg::build_new_tpl(&config)?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?; let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content.as_bytes())?; let msg = Msg::from(content.as_bytes());
input::ask_for_confirmation("Send the message?")?; input::ask_for_confirmation("Send the message?")?;
@ -238,17 +307,18 @@ fn run() -> Result<()> {
smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; smtp::send(&config.smtp, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!"); println!("Done!");
imap_conn.close();
} }
if let Some(matches) = matches.subcommand_matches("reply") { if let Some(matches) = matches.subcommand_matches("reply") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let mut imap_conn = ImapConnector::new(&config.imap)?; let mut imap_conn = ImapConnector::new(&config.imap)?;
let msg = imap_conn.read_msg(&mbox, &uid)?; let mbox = matches.value_of("mailbox").unwrap();
let msg = Msg::from(&msg)?; let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice());
let tpl = if matches.is_present("reply-all") { let tpl = if matches.is_present("reply-all") {
msg.build_reply_all_tpl(&config)? msg.build_reply_all_tpl(&config)?
} else { } else {
@ -256,7 +326,7 @@ fn run() -> Result<()> {
}; };
let content = input::open_editor_with_tpl(&tpl.as_bytes())?; let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content.as_bytes())?; let msg = Msg::from(content.as_bytes());
input::ask_for_confirmation("Send the message?")?; input::ask_for_confirmation("Send the message?")?;
@ -264,20 +334,21 @@ fn run() -> Result<()> {
smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; smtp::send(&config.smtp, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!"); println!("Done!");
imap_conn.close();
} }
if let Some(matches) = matches.subcommand_matches("forward") { if let Some(matches) = matches.subcommand_matches("forward") {
let config = Config::new_from_file()?; let config = Config::new_from_file()?;
let mbox = matches.value_of("mailbox").unwrap();
let uid = matches.value_of("uid").unwrap();
let mut imap_conn = ImapConnector::new(&config.imap)?; let mut imap_conn = ImapConnector::new(&config.imap)?;
let msg = imap_conn.read_msg(&mbox, &uid)?; let mbox = matches.value_of("mailbox").unwrap();
let msg = Msg::from(&msg)?; let uid = matches.value_of("uid").unwrap();
let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?.as_slice());
let tpl = msg.build_forward_tpl(&config)?; let tpl = msg.build_forward_tpl(&config)?;
let content = input::open_editor_with_tpl(&tpl.as_bytes())?; let content = input::open_editor_with_tpl(&tpl.as_bytes())?;
let msg = Msg::from(content.as_bytes())?; let msg = Msg::from(content.as_bytes());
input::ask_for_confirmation("Send the message?")?; input::ask_for_confirmation("Send the message?")?;
@ -285,6 +356,8 @@ fn run() -> Result<()> {
smtp::send(&config.smtp, &msg.to_sendable_msg()?)?; smtp::send(&config.smtp, &msg.to_sendable_msg()?)?;
imap_conn.append_msg("Sent", &msg.to_vec()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?)?;
println!("Done!"); println!("Done!");
imap_conn.close();
} }
Ok(()) Ok(())

View file

@ -83,13 +83,13 @@ impl DisplayCell for Attributes<'_> {
} }
} }
pub struct Mailbox<'a> { pub struct Mbox<'a> {
pub delim: Delim, pub delim: Delim,
pub name: Name, pub name: Name,
pub attributes: Attributes<'a>, pub attributes: Attributes<'a>,
} }
impl Mailbox<'_> { impl Mbox<'_> {
pub fn from_name(name: &imap::types::Name) -> Self { pub fn from_name(name: &imap::types::Name) -> Self {
Self { Self {
delim: Delim::from_name(name), delim: Delim::from_name(name),
@ -99,7 +99,7 @@ impl Mailbox<'_> {
} }
} }
impl<'a> DisplayRow for Mailbox<'a> { impl<'a> DisplayRow for Mbox<'a> {
fn to_row(&self) -> Vec<table::Cell> { fn to_row(&self) -> Vec<table::Cell> {
vec![ vec![
self.delim.to_cell(), self.delim.to_cell(),
@ -109,12 +109,12 @@ impl<'a> DisplayRow for Mailbox<'a> {
} }
} }
impl<'a> DisplayTable<'a, Mailbox<'a>> for Vec<Mailbox<'a>> { impl<'a> DisplayTable<'a, Mbox<'a>> for Vec<Mbox<'a>> {
fn cols() -> &'a [&'a str] { fn cols() -> &'a [&'a str] {
&["delim", "name", "attributes"] &["delim", "name", "attributes"]
} }
fn rows(&self) -> &Vec<Mailbox<'a>> { fn rows(&self) -> &Vec<Mbox<'a>> {
self self
} }
} }

View file

@ -1,7 +1,8 @@
use lettre; use lettre;
use mailparse::{self, MailHeaderMap}; use mailparse::{self, MailHeaderMap};
use std::{fmt, ops, result}; use std::{fmt, result};
use crate::table::{self, DisplayRow, DisplayTable};
use crate::Config; use crate::Config;
// Error wrapper // Error wrapper
@ -9,8 +10,7 @@ use crate::Config;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
ParseMsgError(mailparse::MailParseError), ParseMsgError(mailparse::MailParseError),
BuildEmailError(lettre::error::Error), BuildSendableMsgError(lettre::error::Error),
TryError,
} }
impl fmt::Display for Error { impl fmt::Display for Error {
@ -18,8 +18,7 @@ impl fmt::Display for Error {
write!(f, "(msg): ")?; write!(f, "(msg): ")?;
match self { match self {
Error::ParseMsgError(err) => err.fmt(f), Error::ParseMsgError(err) => err.fmt(f),
Error::BuildEmailError(err) => err.fmt(f), Error::BuildSendableMsgError(err) => err.fmt(f),
Error::TryError => write!(f, "cannot parse"),
} }
} }
} }
@ -32,7 +31,7 @@ impl From<mailparse::MailParseError> for Error {
impl From<lettre::error::Error> for Error { impl From<lettre::error::Error> for Error {
fn from(err: lettre::error::Error) -> Error { fn from(err: lettre::error::Error) -> Error {
Error::BuildEmailError(err) Error::BuildSendableMsgError(err)
} }
} }
@ -40,28 +39,45 @@ impl From<lettre::error::Error> for Error {
type Result<T> = result::Result<T, Error>; type Result<T> = result::Result<T, Error>;
// Wrapper around mailparse::ParsedMail and lettre::Message // Msg
#[derive(Debug)] #[derive(Debug)]
pub struct Msg<'a>(mailparse::ParsedMail<'a>); pub struct Msg {
pub uid: u32,
pub flags: Vec<String>,
raw: Vec<u8>,
}
impl<'a> ops::Deref for Msg<'a> { impl From<&[u8]> for Msg {
type Target = mailparse::ParsedMail<'a>; fn from(item: &[u8]) -> Self {
Self {
fn deref(&self) -> &Self::Target { uid: 0,
&self.0 flags: vec![],
raw: item.to_vec(),
}
} }
} }
impl<'a> Msg<'a> { impl From<&imap::types::Fetch> for Msg {
pub fn from(bytes: &'a [u8]) -> Result<Self> { fn from(fetch: &imap::types::Fetch) -> Self {
Ok(Self(mailparse::parse_mail(bytes)?)) Self {
uid: fetch.uid.unwrap_or_default(),
flags: vec![],
raw: fetch.body().unwrap_or_default().to_vec(),
}
}
}
impl<'a> Msg {
pub fn parse(&'a self) -> Result<mailparse::ParsedMail<'a>> {
Ok(mailparse::parse_mail(&self.raw)?)
} }
pub fn to_vec(&self) -> Result<Vec<u8>> { pub fn to_vec(&self) -> Result<Vec<u8>> {
let headers = self.0.get_headers().get_raw_bytes().to_vec(); let parsed = self.parse()?;
let headers = parsed.get_headers().get_raw_bytes().to_vec();
let sep = "\r\n".as_bytes().to_vec(); let sep = "\r\n".as_bytes().to_vec();
let body = self.0.get_body()?.as_bytes().to_vec(); let body = parsed.get_body()?.as_bytes().to_vec();
Ok(vec![headers, sep, body].concat()) Ok(vec![headers, sep, body].concat())
} }
@ -70,8 +86,8 @@ impl<'a> Msg<'a> {
use lettre::message::header::{ContentTransferEncoding, ContentType}; use lettre::message::header::{ContentTransferEncoding, ContentType};
use lettre::message::{Message, SinglePart}; use lettre::message::{Message, SinglePart};
let msg = self let parsed = self.parse()?;
.0 let msg = parsed
.headers .headers
.iter() .iter()
.fold(Message::builder(), |msg, h| { .fold(Message::builder(), |msg, h| {
@ -111,7 +127,7 @@ impl<'a> Msg<'a> {
SinglePart::builder() SinglePart::builder()
.header(ContentType("text/plain; charset=utf-8".parse().unwrap())) .header(ContentType("text/plain; charset=utf-8".parse().unwrap()))
.header(ContentTransferEncoding::Base64) .header(ContentTransferEncoding::Base64)
.body(self.0.get_body_raw()?), .body(parsed.get_body_raw()?),
)?; )?;
Ok(msg) Ok(msg)
@ -147,7 +163,7 @@ impl<'a> Msg<'a> {
pub fn extract_parts(&self) -> Result<Vec<(String, Vec<u8>)>> { pub fn extract_parts(&self) -> Result<Vec<(String, Vec<u8>)>> {
let mut parts = vec![]; let mut parts = vec![];
Self::extract_parts_into(&self.0, &mut parts); Self::extract_parts_into(&self.parse()?, &mut parts);
Ok(parts) Ok(parts)
} }
@ -167,7 +183,7 @@ impl<'a> Msg<'a> {
} }
pub fn build_reply_tpl(&self, config: &Config) -> Result<String> { pub fn build_reply_tpl(&self, config: &Config) -> Result<String> {
let msg = &self.0; let msg = &self.parse()?;
let headers = msg.get_headers(); let headers = msg.get_headers();
let mut tpl = vec![]; let mut tpl = vec![];
@ -207,7 +223,7 @@ impl<'a> Msg<'a> {
} }
pub fn build_reply_all_tpl(&self, config: &Config) -> Result<String> { pub fn build_reply_all_tpl(&self, config: &Config) -> Result<String> {
let msg = &self.0; let msg = &self.parse()?;
let headers = msg.get_headers(); let headers = msg.get_headers();
let mut tpl = vec![]; let mut tpl = vec![];
@ -289,7 +305,7 @@ impl<'a> Msg<'a> {
} }
pub fn build_forward_tpl(&self, config: &Config) -> Result<String> { pub fn build_forward_tpl(&self, config: &Config) -> Result<String> {
let msg = &self.0; let msg = &self.parse()?;
let headers = msg.get_headers(); let headers = msg.get_headers();
let mut tpl = vec![]; let mut tpl = vec![];
@ -313,3 +329,41 @@ impl<'a> Msg<'a> {
Ok(tpl.join("\r\n")) Ok(tpl.join("\r\n"))
} }
} }
impl DisplayRow for Msg {
fn to_row(&self) -> Vec<table::Cell> {
match self.parse() {
Err(_) => vec![],
Ok(parsed) => {
let headers = parsed.get_headers();
let uid = &self.uid.to_string();
let flags = String::new(); // TODO: render flags
let sender = headers
.get_first_value("reply-to")
.or(headers.get_first_value("from"))
.unwrap_or_default();
let subject = headers.get_first_value("subject").unwrap_or_default();
let date = headers.get_first_value("date").unwrap_or_default();
vec![
table::Cell::new(&[table::RED], &uid),
table::Cell::new(&[table::WHITE], &flags),
table::Cell::new(&[table::BLUE], &sender),
table::Cell::new(&[table::GREEN], &subject),
table::Cell::new(&[table::YELLOW], &date),
]
}
}
}
}
impl<'a> DisplayTable<'a, Msg> for Vec<Msg> {
fn cols() -> &'a [&'a str] {
&["uid", "flags", "sender", "subject", "date"]
}
fn rows(&self) -> &Vec<Msg> {
self
}
}

View file

@ -78,7 +78,7 @@ pub trait DisplayRow {
fn to_row(&self) -> Vec<Cell>; fn to_row(&self) -> Vec<Cell>;
} }
pub trait DisplayTable<'a, T: DisplayRow> { pub trait DisplayTable<'a, T: DisplayRow + 'a> {
fn cols() -> &'a [&'a str]; fn cols() -> &'a [&'a str];
fn rows(&self) -> &Vec<T>; fn rows(&self) -> &Vec<T>;