use std clients

This commit is contained in:
Clément DOUIN
2026-05-06 01:41:12 +02:00
parent c32047d4de
commit 8416a41f99
149 changed files with 2083 additions and 3857 deletions
+9 -5
View File
@@ -1,17 +1,21 @@
use anyhow::Result;
use pimalaya_toolbox::stream::imap::ImapSession;
use io_imap::client::ImapClient;
use crate::{account::Account, config::ImapConfig};
use crate::{account::Account, config::ImapConfig, imap::session::ImapSession};
pub type ImapAccount = Account<ImapConfig>;
impl ImapAccount {
pub fn new_imap_session(&self) -> Result<ImapSession> {
ImapSession::new(
/// Opens the IMAP connection (TCP/TLS/STARTTLS, greeting, SASL),
/// then hands the established stream and context off to a fresh
/// [`ImapClient`].
pub fn new_imap_client(&self) -> Result<ImapClient> {
let session = ImapSession::new(
self.backend.url.clone(),
self.backend.tls.clone().try_into()?,
self.backend.starttls,
self.backend.sasl.clone().try_into()?,
)
)?;
Ok(ImapClient::from_parts(session.stream, session.context))
}
}
+3 -3
View File
@@ -1,10 +1,10 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use crate::imap::{
account::ImapAccount, envelope::command::ImapEnvelopeCommand, flag::command::ImapFlagCommand,
id::ImapIdCommand, mailbox::command::ImapMailboxCommand, message::command::ImapMessageCommand,
account::ImapAccount, envelope::cli::ImapEnvelopeCommand, flag::cli::ImapFlagCommand,
id::ImapIdCommand, mailbox::cli::ImapMailboxCommand, message::cli::ImapMessageCommand,
};
/// IMAP CLI (requires the `imap` cargo feature).
@@ -1,6 +1,6 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use crate::imap::{
account::ImapAccount,
+13 -51
View File
@@ -1,20 +1,13 @@
use std::{
fmt,
io::{Read, Write},
num::NonZeroU32,
};
use std::fmt;
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{Cell, Row, Table};
use io_imap::{
rfc3501::{fetch::*, select::*},
types::{
core::Vec1,
fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
},
use io_imap::types::{
core::Vec1,
fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
};
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::{
@@ -23,8 +16,6 @@ use crate::imap::{
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Get a single IMAP envelope.
///
/// This command displays detailed envelope information for a specific
@@ -47,54 +38,25 @@ pub struct ImapEnvelopeGetCommand {
impl ImapEnvelopeGetCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let Some(id) = NonZeroU32::new(self.id) else {
if self.id == 0 {
bail!("ID must be non-zero");
};
}
let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::Envelope]);
let mut coroutine = ImapMessageFetchFirst::new(imap.context, id, item_names, !self.seq);
let mut arg: Option<&[u8]> = None;
let sequence_set = self.id.to_string().parse()?;
let mut data = client.fetch(sequence_set, item_names, !self.seq)?;
let items = loop {
match coroutine.resume(arg.take()) {
ImapMessageFetchFirstResult::Ok { items, .. } => break items,
ImapMessageFetchFirstResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageFetchFirstResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageFetchFirstResult::Err { err, .. } => bail!("{err}"),
}
let Some((_, items)) = data.pop_first() else {
bail!("No envelope returned for ID {}", self.id);
};
let table = EnvelopeTable {
+16 -87
View File
@@ -1,25 +1,17 @@
use std::{
collections::BTreeMap,
fmt,
io::{Read, Write},
num::NonZeroU32,
};
use std::{collections::BTreeMap, fmt, num::NonZeroU32};
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_imap::{
rfc3501::{fetch::*, select::*, status::*},
types::{
core::Vec1,
envelope::Address,
fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
sequence::{SeqOrUid, Sequence, SequenceSet},
status::{StatusDataItem, StatusDataItemName},
},
use io_imap::types::{
core::Vec1,
envelope::Address,
fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
sequence::{SeqOrUid, Sequence, SequenceSet},
status::{StatusDataItem, StatusDataItemName},
};
use log::debug;
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use rfc2047_decoder::{Decoder, RecoverStrategy};
use serde::Serialize;
@@ -28,8 +20,6 @@ use crate::imap::{
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// List IMAP envelopes from the given mailbox.
///
/// This command displays envelopes for messages in the specified
@@ -61,57 +51,17 @@ pub struct ImapEnvelopeListCommand {
impl ImapEnvelopeListCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
let exists = if self.mailbox_no_select.inner {
let mut coroutine =
ImapMailboxStatus::new(imap.context, mailbox, &[StatusDataItemName::Messages]);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxStatusResult::Ok { context, items } => {
imap.context = context;
break items.into_iter().find_map(|i| match i {
StatusDataItem::Messages(exists) => Some(exists),
_ => None,
});
}
ImapMailboxStatusResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxStatusResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxStatusResult::Err { err, .. } => bail!("{err}"),
}
}
let items = client.status(mailbox, &[StatusDataItemName::Messages])?;
items.into_iter().find_map(|i| match i {
StatusDataItem::Messages(exists) => Some(exists),
_ => None,
})
} else {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, data } => {
imap.context = context;
break data.exists;
}
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
}
client.select(mailbox)?.exists
};
let mut has_sequence = false;
@@ -131,28 +81,7 @@ impl ImapEnvelopeListCommand {
MessageDataItemName::Envelope,
]);
let mut coroutine = ImapMessageFetch::new(
imap.context,
sequence_set,
item_names,
!self.sequence && has_sequence,
);
let mut arg: Option<&[u8]> = None;
let data = loop {
match coroutine.resume(arg.take()) {
ImapMessageFetchResult::Ok { data, .. } => break data,
ImapMessageFetchResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageFetchResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageFetchResult::Err { err, .. } => bail!("{err}"),
}
};
let data = client.fetch(sequence_set, item_names, !self.sequence && has_sequence)?;
let table = EnvelopesTable {
preset: account.table_preset,
+1 -1
View File
@@ -1,4 +1,4 @@
pub mod command;
pub mod cli;
pub mod get;
pub mod list;
pub mod search;
+9 -52
View File
@@ -1,20 +1,14 @@
use std::{
fmt,
io::{Read, Write},
};
use std::fmt;
use anyhow::{anyhow, bail, Result};
use clap::Parser;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_imap::{
rfc3501::{search::*, select::*},
types::{
core::{AString, Vec1},
datetime::NaiveDate,
search::SearchKey,
},
use io_imap::types::{
core::{AString, Vec1},
datetime::NaiveDate,
search::SearchKey,
};
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::{
@@ -22,8 +16,6 @@ use crate::imap::{
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Search IMAP messages by criteria.
///
/// This command searches for messages matching the given criteria and
@@ -67,50 +59,15 @@ pub struct ImapEnvelopeSearchCommand {
impl ImapEnvelopeSearchCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let criteria = parse_query(&self.query)?;
let mut coroutine = ImapMessageSearch::new(imap.context, criteria, !self.seq);
let mut arg: Option<&[u8]> = None;
let ids = loop {
match coroutine.resume(arg.take()) {
ImapMessageSearchResult::Ok { ids, .. } => break ids,
ImapMessageSearchResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageSearchResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageSearchResult::Err { err, .. } => bail!("{err}"),
}
};
let ids = client.search(criteria, !self.seq)?;
let table = SearchTable {
preset: account.table_preset,
+8 -57
View File
@@ -1,28 +1,19 @@
use std::{
fmt,
io::{Read, Write},
};
use std::fmt;
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table};
use io_imap::{
rfc3501::select::*,
rfc5256::sort::*,
types::{
core::Vec1,
extensions::sort::{SortCriterion, SortKey},
},
use io_imap::types::{
core::Vec1,
extensions::sort::{SortCriterion, SortKey},
};
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::{
account::ImapAccount, envelope::search::parse_query, mailbox::arg::MailboxNameOptionalArg,
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Sort messages by criteria.
///
/// This command searches for messages matching the given query and
@@ -61,59 +52,19 @@ pub struct ImapEnvelopeSortCommand {
impl ImapEnvelopeSortCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
client.select(mailbox)?;
// SELECT mailbox
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
let context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
// Parse sort criteria
let sort_key = parse_sort_key(&self.sort)?;
let sort_criteria = Vec1::unvalidated(vec![SortCriterion {
reverse: self.reverse,
key: sort_key,
}]);
// Parse search criteria
let search_criteria = parse_query(&self.query)?;
// SORT
let mut coroutine =
ImapMailboxSort::new(context, sort_criteria, search_criteria, !self.seq);
let mut arg: Option<&[u8]> = None;
let ids = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSortResult::Ok { ids, .. } => break ids,
ImapMailboxSortResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSortResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSortResult::Err { err, .. } => bail!("{err}"),
}
};
let ids = client.sort(sort_criteria, search_criteria, !self.seq)?;
let table = SortResultsTable::new(ids, !self.seq);
+9 -78
View File
@@ -1,22 +1,16 @@
use std::{
collections::HashMap,
fmt,
io::{Read, Write},
num::NonZeroU32,
};
use std::{collections::HashMap, fmt, num::NonZeroU32};
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
rfc3501::{fetch::*, select::*},
rfc5256::thread::*,
client::ImapClient,
types::{
extensions::thread::{Thread, ThreadingAlgorithm},
fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
sequence::SequenceSet,
},
};
use pimalaya_toolbox::{stream::imap::ImapSession, terminal::printer::Printer};
use pimalaya_cli::printer::Printer;
use serde::{ser::SerializeStruct, Serialize, Serializer};
use crate::imap::{
@@ -25,8 +19,6 @@ use crate::imap::{
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Thread IMAP messages by algorithm.
///
/// This command groups messages into conversation threads using the
@@ -57,64 +49,21 @@ pub struct ImapEnvelopeThreadCommand {
impl ImapEnvelopeThreadCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let algorithm = parse_algorithm(&self.algorithm)?;
let search_criteria = parse_query(&self.query)?;
let mut coroutine =
ImapMessageThread::new(imap.context, algorithm, search_criteria, !self.seq);
let mut arg: Option<&[u8]> = None;
let threads = client.thread(algorithm, search_criteria, !self.seq)?;
let threads = loop {
match coroutine.resume(arg.take()) {
ImapMessageThreadResult::Ok {
context, threads, ..
} => {
imap.context = context;
break threads;
}
ImapMessageThreadResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageThreadResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageThreadResult::Err { err, .. } => bail!("{err}"),
}
};
// Collect all message IDs from threads to fetch subjects
let all_ids = collect_thread_ids(&threads);
// Fetch subjects for all messages in threads
let subjects = if !all_ids.is_empty() {
fetch_subjects(imap, &all_ids, !self.seq)?
fetch_subjects(&mut client, &all_ids, !self.seq)?
} else {
HashMap::new()
};
@@ -162,7 +111,7 @@ fn collect_thread_ids_recursive(thread: &Thread, ids: &mut Vec<NonZeroU32>) {
}
fn fetch_subjects(
mut imap: ImapSession,
client: &mut ImapClient,
ids: &[NonZeroU32],
uid: bool,
) -> Result<HashMap<u32, String>> {
@@ -170,7 +119,6 @@ fn fetch_subjects(
return Ok(HashMap::new());
}
// Build sequence set from IDs
let seq_set_str = ids
.iter()
.map(|id| id.to_string())
@@ -183,24 +131,7 @@ fn fetch_subjects(
MessageDataItemName::Uid,
]);
let mut coroutine = ImapMessageFetch::new(imap.context, sequence_set, item_names, uid);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
let data = loop {
match coroutine.resume(arg.take()) {
ImapMessageFetchResult::Ok { data, .. } => break data,
ImapMessageFetchResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageFetchResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageFetchResult::Err { err, .. } => bail!("{err}"),
}
};
let data = client.fetch(sequence_set, item_names, uid)?;
let mut subjects: HashMap<u32, String> = HashMap::new();
+8 -55
View File
@@ -1,23 +1,16 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::{
rfc3501::{select::*, store::*},
types::{
flag::{Flag, StoreType},
IntoStatic,
},
use io_imap::types::{
flag::{Flag, StoreType},
IntoStatic,
};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Add IMAP flag(s) to message(s).
///
/// This command adds the given flags to messages identified by the
@@ -43,29 +36,11 @@ pub struct ImapFlagAddCommand {
impl ImapFlagAddCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let sequence_set = self.sequence_set.as_str().try_into()?;
@@ -75,29 +50,7 @@ impl ImapFlagAddCommand {
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?;
let mut coroutine = ImapMessageStoreSilent::new(
imap.context,
sequence_set,
StoreType::Add,
flags,
!self.seq,
);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMessageStoreSilentResult::Ok(_) => break,
ImapMessageStoreSilentResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageStoreSilentResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageStoreSilentResult::Err { err, .. } => bail!("{err}"),
}
}
client.store(sequence_set, StoreType::Add, flags, !self.seq)?;
printer.out(Message::new("Flag(s) successfully added"))
}
@@ -1,6 +1,6 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use crate::imap::{
account::ImapAccount,
+8 -37
View File
@@ -1,23 +1,14 @@
use std::{
collections::BTreeMap,
fmt,
io::{Read, Write},
};
use std::{collections::BTreeMap, fmt};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use comfy_table::{Cell, ContentArrangement, Row, Table};
use io_imap::{
rfc3501::select::*,
types::flag::{Flag, FlagPerm},
};
use pimalaya_toolbox::terminal::printer::Printer;
use io_imap::types::flag::{Flag, FlagPerm};
use pimalaya_cli::printer::Printer;
use serde::{Serialize, Serializer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// List available IMAP flags for the given mailbox.
///
/// This command displays the flags and permanent flags that are
@@ -31,32 +22,12 @@ pub struct ImapFlagListCommand {
impl ImapFlagListCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
let (flags, permanent_flags) = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { data, .. } => {
break (
data.flags.unwrap_or_default(),
data.permanent_flags.unwrap_or_default(),
)
}
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
let data = client.select(mailbox)?;
let flags = data.flags.unwrap_or_default();
let permanent_flags = data.permanent_flags.unwrap_or_default();
let table = FlagsTable {
preset: account.table_preset,
+1 -1
View File
@@ -1,5 +1,5 @@
pub mod add;
pub mod command;
pub mod cli;
pub mod list;
pub mod remove;
pub mod set;
+8 -55
View File
@@ -1,23 +1,16 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::{
rfc3501::{select::*, store::*},
types::{
flag::{Flag, StoreType},
IntoStatic,
},
use io_imap::types::{
flag::{Flag, StoreType},
IntoStatic,
};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Remove IMAP flag(s) from message(s).
///
/// This command removes the specified flag(s) from message(s)
@@ -43,29 +36,11 @@ pub struct ImapFlagRemoveCommand {
impl ImapFlagRemoveCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let sequence_set = self.sequence_set.as_str().try_into()?;
@@ -75,29 +50,7 @@ impl ImapFlagRemoveCommand {
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?;
let mut coroutine = ImapMessageStoreSilent::new(
imap.context,
sequence_set,
StoreType::Remove,
flags,
!self.seq,
);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMessageStoreSilentResult::Ok(_) => break,
ImapMessageStoreSilentResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageStoreSilentResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageStoreSilentResult::Err { err, .. } => bail!("{err}"),
}
}
client.store(sequence_set, StoreType::Remove, flags, !self.seq)?;
printer.out(Message::new("Flag(s) successfully removed"))
}
+8 -55
View File
@@ -1,23 +1,16 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::{
rfc3501::{select::*, store::*},
types::{
flag::{Flag, StoreType},
IntoStatic,
},
use io_imap::types::{
flag::{Flag, StoreType},
IntoStatic,
};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Set IMAP flag(s) on message(s), replacing any existing flags.
///
/// This command replaces all existing flags on messages identified by
@@ -43,29 +36,11 @@ pub struct ImapFlagSetCommand {
impl ImapFlagSetCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let sequence_set = self.sequence_set.as_str().try_into()?;
@@ -75,29 +50,7 @@ impl ImapFlagSetCommand {
.map(|f| Flag::try_from(f.as_str()).map(|flag| flag.into_static()))
.collect::<Result<_, _>>()?;
let mut coroutine = ImapMessageStoreSilent::new(
imap.context,
sequence_set,
StoreType::Replace,
flags,
!self.seq,
);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMessageStoreSilentResult::Ok(_) => break,
ImapMessageStoreSilentResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageStoreSilentResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageStoreSilentResult::Err { err, .. } => bail!("{err}"),
}
}
client.store(sequence_set, StoreType::Replace, flags, !self.seq)?;
printer.out(Message::new("Flag(s) successfully replaced"))
}
+8 -34
View File
@@ -1,26 +1,17 @@
use std::{
collections::HashMap,
fmt,
io::{Read, Write},
};
use std::{collections::HashMap, fmt};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use comfy_table::{Cell, Row, Table};
use io_imap::{
rfc2971::id::*,
types::{
core::{IString, NString},
IntoStatic,
},
use io_imap::types::{
core::{IString, NString},
IntoStatic,
};
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::account::ImapAccount;
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Get information about the IMAP server.
///
/// This command allows you to exchange parameters with the IMAP
@@ -37,7 +28,7 @@ pub struct ImapIdCommand {
impl ImapIdCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mut params = HashMap::new();
params.extend([
@@ -63,24 +54,7 @@ impl ImapIdCommand {
params.extend(more);
}
let mut coroutine = ImapServerId::new(imap.context, Some(params.into_iter().collect()));
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
let params = loop {
match coroutine.resume(arg.take()) {
ImapServerIdResult::Ok { server_id, .. } => break server_id,
ImapServerIdResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapServerIdResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapServerIdResult::Err { err, .. } => bail!("{err}"),
}
};
let params = client.id(Some(params.into_iter().collect()))?;
let table = ServerIdTable {
preset: account.table_preset,
@@ -1,6 +1,6 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use crate::imap::{
account::ImapAccount,
+4 -28
View File
@@ -1,14 +1,9 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::rfc3501::close::*;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::account::ImapAccount;
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Close the current, selected mailbox.
///
/// This command also expunges the current mailbox and returns to the
@@ -23,27 +18,8 @@ pub struct ImapMailboxCloseCommand;
impl ImapMailboxCloseCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut close_coroutine = ImapMailboxClose::new(imap.context);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match close_coroutine.resume(arg.take()) {
ImapMailboxCloseResult::Ok(_) => break,
ImapMailboxCloseResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxCloseResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxCloseResult::Err { err, .. } => bail!("{err}"),
}
}
let mut client = account.new_imap_client()?;
client.close()?;
printer.out(Message::new("Mailbox successfully closed"))
}
}
+4 -29
View File
@@ -1,14 +1,9 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::rfc3501::create::*;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Create the given mailbox.
///
/// This command allows you to create a new mailbox using the given
@@ -21,29 +16,9 @@ pub struct ImapMailboxCreateCommand {
impl ImapMailboxCreateCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut coroutine = ImapMailboxCreate::new(imap.context, mailbox);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxCreateResult::Ok(_) => break,
ImapMailboxCreateResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxCreateResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxCreateResult::Err { err, .. } => bail!("{err}"),
}
}
client.create(mailbox)?;
printer.out(Message::new("Mailbox successfully created"))
}
}
+4 -28
View File
@@ -1,14 +1,9 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::rfc3501::delete::*;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Delete the given mailbox.
///
/// All emails from the given mailbox are definitely deleted. The
@@ -21,28 +16,9 @@ pub struct ImapMailboxDeleteCommand {
impl ImapMailboxDeleteCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut coroutine = ImapMailboxDelete::new(imap.context, mailbox);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxDeleteResult::Ok(_) => break,
ImapMailboxDeleteResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxDeleteResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxDeleteResult::Err { err, .. } => bail!("{err}"),
}
}
client.delete(mailbox)?;
printer.out(Message::new("Mailbox successfully deleted"))
}
}
+5 -44
View File
@@ -1,17 +1,12 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::rfc3501::{expunge::*, select::*};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameArg, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Expunge the given mailbox.
///
/// All envelopes with the \Deleted flag will be definitely removed
@@ -26,48 +21,14 @@ pub struct ImapMailboxExpungeCommand {
impl ImapMailboxExpungeCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let mut coroutine = ImapMailboxExpunge::new(imap.context);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxExpungeResult::Ok { .. } => break,
ImapMailboxExpungeResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxExpungeResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxExpungeResult::Err { err, .. } => bail!("{err}"),
}
}
client.expunge()?;
printer.out(Message::new("Mailbox successfully expunged"))
}
+7 -49
View File
@@ -1,22 +1,14 @@
use std::{
fmt,
io::{Read, Write},
};
use std::fmt;
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use comfy_table::{Cell, Row, Table};
use io_imap::{
rfc3501::{list::*, lsub::*},
types::{core::QuotedChar, flag::FlagNameAttribute, mailbox::Mailbox},
};
use pimalaya_toolbox::terminal::printer::Printer;
use io_imap::types::{core::QuotedChar, flag::FlagNameAttribute, mailbox::Mailbox};
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::account::ImapAccount;
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// List, search and filter mailboxes.
///
/// This command allows you to list mailboxes from your IMAP account.
@@ -39,48 +31,14 @@ pub struct ImapMailboxListCommand {
impl ImapMailboxListCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let reference = self.reference.try_into()?;
let pattern = self.pattern.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
let mailboxes = if self.all {
let mut coroutine = ImapMailboxList::new(imap.context, reference, pattern);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxListResult::Ok { mailboxes, .. } => break mailboxes,
ImapMailboxListResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxListResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxListResult::Err { err, .. } => bail!("{err}"),
}
}
client.list(reference, pattern)?
} else {
let mut coroutine = ImapMailboxLsub::new(imap.context, reference, pattern);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxLsubResult::Ok { mailboxes, .. } => break mailboxes,
ImapMailboxLsubResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxLsubResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxLsubResult::Err { err, .. } => bail!("{err}"),
}
}
client.lsub(reference, pattern)?
};
let table = MailboxesTable {
+1 -1
View File
@@ -1,6 +1,6 @@
pub mod arg;
pub mod cli;
pub mod close;
pub mod command;
pub mod create;
pub mod delete;
pub mod expunge;
+8 -67
View File
@@ -1,20 +1,13 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::{
rfc3501::{expunge::*, select::*, store::*},
types::flag::{Flag, StoreType},
};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use io_imap::types::flag::{Flag, StoreType};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameArg, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Shortcut for marking as deleted all envelopes then expunging the
/// given mailbox.
///
@@ -30,72 +23,20 @@ pub struct ImapMailboxPurgeCommand {
impl ImapMailboxPurgeCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let mut coroutine = ImapMessageStoreSilent::new(
imap.context,
client.store(
"1:*".try_into()?,
StoreType::Add,
vec![Flag::Deleted],
false,
);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMessageStoreSilentResult::Ok(context) => break context,
ImapMessageStoreSilentResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageStoreSilentResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageStoreSilentResult::Err { err, .. } => bail!("{err}"),
}
};
let mut coroutine = ImapMailboxExpunge::new(imap.context);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxExpungeResult::Ok { .. } => break,
ImapMailboxExpungeResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxExpungeResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxExpungeResult::Err { err, .. } => bail!("{err}"),
}
}
)?;
client.expunge()?;
printer.out(Message::new("Mailbox successfully purged"))
}
+4 -28
View File
@@ -1,17 +1,12 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::rfc3501::rename::*;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameArg, TargetMailboxNameArg},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Rename the given mailbox.
///
/// This command renames an existing mailbox to a new name.
@@ -25,29 +20,10 @@ pub struct ImapMailboxRenameCommand {
impl ImapMailboxRenameCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let from = self.mailbox_source_name.inner.try_into()?;
let to = self.mailbox_dest_name.inner.try_into()?;
let mut coroutine = ImapMailboxRename::new(imap.context, from, to);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxRenameResult::Ok(_) => break,
ImapMailboxRenameResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxRenameResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxRenameResult::Err { err, .. } => bail!("{err}"),
}
}
client.rename(from, to)?;
printer.out(Message::new("Mailbox successfully renamed"))
}
}
+4 -28
View File
@@ -1,14 +1,9 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::rfc3501::select::*;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Select the given mailbox.
///
/// This command permanently removes all messages with the \Deleted
@@ -25,28 +20,9 @@ pub struct ImapMailboxSelectCommand {
impl ImapMailboxSelectCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { .. } => break,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
}
client.select(mailbox)?;
printer.out(Message::new("Mailbox successfully selected"))
}
}
+6 -31
View File
@@ -1,22 +1,14 @@
use std::{
fmt,
io::{Read, Write},
};
use std::fmt;
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use comfy_table::{Cell, Row, Table};
use io_imap::{
rfc3501::status::*,
types::status::{StatusDataItem, StatusDataItemName},
};
use pimalaya_toolbox::terminal::printer::Printer;
use io_imap::types::status::{StatusDataItem, StatusDataItemName};
use pimalaya_cli::printer::Printer;
use serde::{Serialize, Serializer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Get the status of the given mailbox.
///
/// This command displays status information about a mailbox,
@@ -29,7 +21,7 @@ pub struct ImapMailboxStatusCommand {
impl ImapMailboxStatusCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let item_names = vec![
StatusDataItemName::Messages,
@@ -39,24 +31,7 @@ impl ImapMailboxStatusCommand {
StatusDataItemName::UidValidity,
];
let mut coroutine = ImapMailboxStatus::new(imap.context, mailbox, item_names);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
let items = loop {
match coroutine.resume(arg.take()) {
ImapMailboxStatusResult::Ok { items, .. } => break items,
ImapMailboxStatusResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxStatusResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxStatusResult::Err { err, .. } => bail!("{err}"),
}
};
let items = client.status(mailbox, item_names)?;
let table = MailboxStatusTable {
preset: account.table_preset,
+4 -28
View File
@@ -1,14 +1,9 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::rfc3501::subscribe::*;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Subscribe to the given mailbox.
///
/// This command subscribes to a mailbox, making it appear in the
@@ -21,28 +16,9 @@ pub struct ImapMailboxSubscribeCommand {
impl ImapMailboxSubscribeCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut coroutine = ImapMailboxSubscribe::new(imap.context, mailbox);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxSubscribeResult::Ok(_) => break,
ImapMailboxSubscribeResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSubscribeResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSubscribeResult::Err { err, .. } => bail!("{err}"),
}
}
client.subscribe(mailbox)?;
printer.out(Message::new("Mailbox successfully subscribed"))
}
}
+4 -28
View File
@@ -1,14 +1,9 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::rfc3691::unselect::*;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::account::ImapAccount;
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Unselect a current, selected mailbox.
///
/// Unlike CLOSE, UNSELECT does not expunge deleted messages.
@@ -22,27 +17,8 @@ pub struct ImapMailboxUnselectCommand;
impl ImapMailboxUnselectCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut coroutine = ImapMailboxUnselect::new(imap.context);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxUnselectResult::Ok(_) => break,
ImapMailboxUnselectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxUnselectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxUnselectResult::Err { err, .. } => bail!("{err}"),
}
}
let mut client = account.new_imap_client()?;
client.unselect()?;
printer.out(Message::new("Mailbox successfully unselected"))
}
}
+4 -28
View File
@@ -1,14 +1,9 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::rfc3501::unsubscribe::*;
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Unsubscribe from the given mailbox.
///
/// This command unsubscribes from a mailbox, removing it from the
@@ -21,28 +16,9 @@ pub struct ImapMailboxUnsubscribeCommand {
impl ImapMailboxUnsubscribeCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut coroutine = ImapMailboxUnsubscribe::new(imap.context, mailbox);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMailboxUnsubscribeResult::Ok(_) => break,
ImapMailboxUnsubscribeResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxUnsubscribeResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxUnsubscribeResult::Err { err, .. } => bail!("{err}"),
}
}
client.unsubscribe(mailbox)?;
printer.out(Message::new("Mailbox successfully unsubscribed"))
}
}
@@ -1,6 +1,6 @@
use anyhow::Result;
use clap::Subcommand;
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use crate::imap::{
account::ImapAccount,
+6 -48
View File
@@ -1,20 +1,13 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::{
rfc3501::{copy::*, select::*},
types::mailbox::Mailbox,
};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use io_imap::types::mailbox::Mailbox;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag, TargetMailboxNameArg},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Copy IMAP message(s) to the given mailbox.
///
/// This command copies message(s) identified by the given sequence
@@ -39,52 +32,17 @@ pub struct ImapMessageCopyCommand {
impl ImapMessageCopyCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let sequence_set = self.sequence_set.as_str().try_into()?;
let destination: Mailbox = self.mailbox_dest_name.inner.try_into()?;
let mut coroutine =
ImapMessageCopy::new(imap.context, sequence_set, destination, !self.seq);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMessageCopyResult::Ok { .. } => break,
ImapMessageCopyResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageCopyResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageCopyResult::Err { err, .. } => bail!("{err}"),
}
}
client.copy(sequence_set, destination, !self.seq)?;
printer.out(Message::new("Message(s) successfully copied"))
}
+14 -49
View File
@@ -1,23 +1,17 @@
use std::{
fs,
io::{self, Read, Write},
num::NonZeroU32,
io::{self, Write},
path::PathBuf,
};
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
rfc3501::{fetch::*, select::*},
types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
};
use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName};
use mail_parser::{MessageParser, MimeHeaders};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameOptionalFlag};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Export type for message export.
#[derive(Debug, Clone, clap::ValueEnum)]
pub enum ExportType {
@@ -63,34 +57,14 @@ pub struct ImapMessageExportCommand {
impl ImapMessageExportCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
client.select(mailbox)?;
// SELECT mailbox
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
let context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
// FETCH with BODY.PEEK[] to avoid marking as read
let Some(id) = NonZeroU32::new(self.id) else {
if self.id == 0 {
bail!("Export message error: ID must be non-zero");
};
}
let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt {
@@ -99,25 +73,16 @@ impl ImapMessageExportCommand {
peek: true,
}]);
let mut coroutine = ImapMessageFetchFirst::new(context, id, item_names, !self.seq);
let mut arg: Option<&[u8]> = None;
let sequence_set = self.id.to_string().parse()?;
let mut data = client.fetch(sequence_set, item_names, !self.seq)?;
let items = loop {
match coroutine.resume(arg.take()) {
ImapMessageFetchFirstResult::Ok { items, .. } => break items,
ImapMessageFetchFirstResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageFetchFirstResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageFetchFirstResult::Err { err, .. } => bail!("{err}"),
}
let Some((_, items)) = data.pop_first() else {
bail!(
"Export message `{}` error: no message data returned",
self.id
);
};
// Extract raw message bytes
let mut raw_message: Option<Vec<u8>> = None;
for item in items.into_iter() {
if let MessageDataItem::BodyExt { data, .. } = item {
+11 -49
View File
@@ -1,18 +1,11 @@
use std::{
fmt,
io::{Read, Write},
num::NonZeroU32,
};
use std::fmt;
use anyhow::{bail, Result};
use clap::Parser;
use comfy_table::{presets, Cell, ContentArrangement, Row, Table};
use io_imap::{
rfc3501::{fetch::*, select::*},
types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
};
use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName};
use mail_parser::{Addr, Address, ContentType, Message, MessageParser, MimeHeaders};
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::{
@@ -20,8 +13,6 @@ use crate::imap::{
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Get a message and display its structure.
///
/// This command fetches a message and displays its headers along with
@@ -42,32 +33,14 @@ pub struct ImapMessageGetCommand {
impl ImapMessageGetCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let Some(id) = NonZeroU32::new(self.id) else {
if self.id == 0 {
bail!("ID must be non-zero");
};
let mut buf = [0u8; READ_BUFFER_SIZE];
}
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let item_names =
@@ -77,22 +50,11 @@ impl ImapMessageGetCommand {
peek: true,
}]);
let mut coroutine = ImapMessageFetchFirst::new(imap.context, id, item_names, !self.seq);
let mut arg: Option<&[u8]> = None;
let sequence_set = self.id.to_string().parse()?;
let mut data = client.fetch(sequence_set, item_names, !self.seq)?;
let items = loop {
match coroutine.resume(arg.take()) {
ImapMessageFetchFirstResult::Ok { items, .. } => break items,
ImapMessageFetchFirstResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageFetchFirstResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageFetchFirstResult::Err { err, .. } => bail!("{err}"),
}
let Some((_, items)) = data.pop_first() else {
bail!("Get message `{}` error: no message data returned", self.id);
};
let mut raw_message: Option<Vec<u8>> = None;
+1 -1
View File
@@ -1,4 +1,4 @@
pub mod command;
pub mod cli;
pub mod copy;
pub mod export;
pub mod get;
+6 -45
View File
@@ -1,17 +1,13 @@
use std::io::{Read, Write};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::{rfc3501::select::*, rfc6851::r#move::*, types::mailbox::Mailbox};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use io_imap::types::mailbox::Mailbox;
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{
account::ImapAccount,
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag, TargetMailboxNameArg},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Move message(s) to the given mailbox.
///
/// This command moves messages identified by the given sequence set
@@ -37,52 +33,17 @@ pub struct ImapMessageMoveCommand {
impl ImapMessageMoveCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let sequence_set = self.sequence_set.as_str().try_into()?;
let destination: Mailbox<'static> = self.mailbox_dest_name.inner.try_into()?;
let mut coroutine =
ImapMessageMove::new(imap.context, sequence_set, destination, !self.seq);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMessageMoveResult::Ok { .. } => break,
ImapMessageMoveResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageMoveResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageMoveResult::Err { err, .. } => bail!("{err}"),
}
}
client.r#move(sequence_set, destination, !self.seq)?;
printer.out(Message::new("Message(s) successfully moved"))
}
+11 -49
View File
@@ -1,17 +1,10 @@
use std::{
fmt,
io::{Read, Write},
num::NonZeroU32,
};
use std::fmt;
use anyhow::{bail, Result};
use clap::Parser;
use io_imap::{
rfc3501::{fetch::*, select::*},
types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName},
};
use io_imap::types::fetch::{MacroOrMessageDataItemNames, MessageDataItem, MessageDataItemName};
use mail_parser::{Message, MessageParser};
use pimalaya_toolbox::terminal::printer::Printer;
use pimalaya_cli::printer::Printer;
use serde::Serialize;
use crate::imap::{
@@ -19,8 +12,6 @@ use crate::imap::{
mailbox::arg::{MailboxNameOptionalFlag, MailboxNoSelectFlag},
};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Read message content.
///
/// This command fetches a message and displays its text content.
@@ -47,34 +38,16 @@ pub struct ImapMessageReadCommand {
impl ImapMessageReadCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox = self.mailbox_name.inner.try_into()?;
let mut buf = [0u8; READ_BUFFER_SIZE];
if !self.mailbox_no_select.inner {
let mut coroutine = ImapMailboxSelect::new(imap.context, mailbox);
let mut arg: Option<&[u8]> = None;
imap.context = loop {
match coroutine.resume(arg.take()) {
ImapMailboxSelectResult::Ok { context, .. } => break context,
ImapMailboxSelectResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMailboxSelectResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMailboxSelectResult::Err { err, .. } => bail!("{err}"),
}
};
client.select(mailbox)?;
}
let Some(id) = NonZeroU32::new(self.id) else {
if self.id == 0 {
bail!("ID must be non-zero");
};
}
let item_names =
MacroOrMessageDataItemNames::MessageDataItemNames(vec![MessageDataItemName::BodyExt {
@@ -83,22 +56,11 @@ impl ImapMessageReadCommand {
peek: true,
}]);
let mut coroutine = ImapMessageFetchFirst::new(imap.context, id, item_names, !self.seq);
let mut arg: Option<&[u8]> = None;
let sequence_set = self.id.to_string().parse()?;
let mut data = client.fetch(sequence_set, item_names, !self.seq)?;
let items = loop {
match coroutine.resume(arg.take()) {
ImapMessageFetchFirstResult::Ok { items, .. } => break items,
ImapMessageFetchFirstResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageFetchFirstResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageFetchFirstResult::Err { err, .. } => bail!("{err}"),
}
let Some((_, items)) = data.pop_first() else {
bail!("Read message `{}` error: no message data returned", self.id);
};
let mut raw_message: Option<Vec<u8>> = None;
+7 -30
View File
@@ -1,20 +1,14 @@
use std::io::{stdin, BufRead, IsTerminal, Read, Write};
use std::io::{stdin, BufRead, IsTerminal};
use anyhow::{bail, Result};
use anyhow::Result;
use clap::Parser;
use io_imap::{
rfc3501::append::*,
types::{
core::Literal, extensions::binary::LiteralOrLiteral8, flag::Flag, mailbox::Mailbox,
IntoStatic,
},
use io_imap::types::{
core::Literal, extensions::binary::LiteralOrLiteral8, flag::Flag, mailbox::Mailbox, IntoStatic,
};
use pimalaya_toolbox::terminal::printer::{Message, Printer};
use pimalaya_cli::printer::{Message, Printer};
use crate::imap::{account::ImapAccount, mailbox::arg::MailboxNameArg};
const READ_BUFFER_SIZE: usize = 16 * 1024;
/// Save a message to a mailbox.
///
/// This command appends a message to the specified mailbox. The
@@ -36,7 +30,7 @@ pub struct ImapMessageSaveCommand {
impl ImapMessageSaveCommand {
pub fn execute(self, printer: &mut impl Printer, account: ImapAccount) -> Result<()> {
let mut imap = account.new_imap_session()?;
let mut client = account.new_imap_client()?;
let mailbox: Mailbox<'static> = self.mailbox.inner.try_into()?;
let message = if !self.message.is_empty() || stdin().is_terminal() || printer.is_json() {
self.message
@@ -61,24 +55,7 @@ impl ImapMessageSaveCommand {
.map(|f| Flag::try_from(f).map(IntoStatic::into_static))
.collect::<Result<_, _>>()?;
let mut coroutine = ImapMessageAppend::new(imap.context, mailbox, flags, None, message);
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapMessageAppendResult::Ok { .. } => break,
ImapMessageAppendResult::WantsRead => {
let n = imap.stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapMessageAppendResult::WantsWrite(bytes) => {
imap.stream.write_all(&bytes)?;
arg = None;
}
ImapMessageAppendResult::Err { err, .. } => bail!("{err}"),
}
}
client.append(mailbox, flags, None, message)?;
printer.out(Message::new("Message successfully saved"))
}
+2 -1
View File
@@ -1,7 +1,8 @@
pub mod account;
pub mod command;
pub mod cli;
pub mod envelope;
pub mod flag;
pub mod id;
pub mod mailbox;
pub mod message;
pub mod session;
+236
View File
@@ -0,0 +1,236 @@
//! Transitional IMAP session helper ported from `pimalaya-toolbox`.
//!
//! Will be replaced by `io_imap::client::ImapClient` once the
//! protocol-specific subcommands switch over.
#[cfg(unix)]
use std::os::unix::net::UnixStream;
use std::{
io::{Read, Write},
net::TcpStream,
};
use anyhow::{bail, Result};
use io_imap::{
context::ImapContext,
rfc3501::{
capability::{ImapCapabilityGet, ImapCapabilityGetResult},
greeting_with_capability::{
ImapGreetingWithCapabilityGet, ImapGreetingWithCapabilityGetResult,
},
login::{ImapSessionLogin, ImapSessionLoginParams, ImapSessionLoginResult},
starttls::{ImapStartTls, ImapStartTlsResult},
},
sasl::authenticate_plain::{
ImapSessionAuthenticatePlain, ImapSessionAuthenticatePlainParams,
ImapSessionAuthenticatePlainResult,
},
types::response::Capability,
};
use log::info;
use pimalaya_stream::{
sasl::{Sasl, SaslMechanism},
std::stream::Stream,
tls::{upgrade_tls, Tls},
};
#[cfg(windows)]
use uds_windows::UnixStream;
use url::Url;
const READ_BUFFER_SIZE: usize = 16 * 1024;
#[derive(Debug)]
pub struct ImapSession {
pub context: ImapContext,
pub stream: Stream,
}
fn drive_greeting_with_capability<S: Read + Write>(
stream: &mut S,
context: ImapContext,
) -> Result<ImapContext> {
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut coroutine = ImapGreetingWithCapabilityGet::new(context);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapGreetingWithCapabilityGetResult::Ok(context) => return Ok(context),
ImapGreetingWithCapabilityGetResult::WantsRead => {
let n = stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapGreetingWithCapabilityGetResult::WantsWrite(bytes) => {
stream.write_all(&bytes)?;
arg = None;
}
ImapGreetingWithCapabilityGetResult::Err { err, .. } => bail!(err),
}
}
}
fn drive_capability<S: Read + Write>(stream: &mut S, context: ImapContext) -> Result<ImapContext> {
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut coroutine = ImapCapabilityGet::new(context);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapCapabilityGetResult::Ok(context) => return Ok(context),
ImapCapabilityGetResult::WantsRead => {
let n = stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapCapabilityGetResult::WantsWrite(bytes) => {
stream.write_all(&bytes)?;
arg = None;
}
ImapCapabilityGetResult::Err { err, .. } => bail!(err),
}
}
}
fn drive_starttls<S: Read + Write>(stream: &mut S, context: ImapContext) -> Result<ImapContext> {
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut coroutine = ImapStartTls::new(context);
let mut arg: Option<&[u8]> = None;
loop {
match coroutine.resume(arg.take()) {
ImapStartTlsResult::WantsStartTls { context, .. } => return Ok(context),
ImapStartTlsResult::WantsRead => {
let n = stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapStartTlsResult::WantsWrite(bytes) => {
stream.write_all(&bytes)?;
arg = None;
}
ImapStartTlsResult::Err { err, .. } => bail!(err),
}
}
}
impl ImapSession {
pub fn new(url: Url, tls: Tls, starttls: bool, mut sasl: Sasl) -> Result<Self> {
info!("connecting to IMAP server using {url}");
let context = ImapContext::new();
let host = url.host_str().unwrap_or("127.0.0.1");
let (mut context, mut stream) = match url.scheme() {
scheme if scheme.eq_ignore_ascii_case("imap") => {
let port = url.port().unwrap_or(143);
let mut tcp = TcpStream::connect((host, port))?;
let context = drive_greeting_with_capability(&mut tcp, context)?;
(context, Stream::Tcp(tcp))
}
scheme if scheme.eq_ignore_ascii_case("imaps") => {
let port = url.port().unwrap_or(993);
let mut tcp = TcpStream::connect((host, port))?;
let context = if starttls {
drive_starttls(&mut tcp, context)?
} else {
context
};
let mut stream = upgrade_tls(host, tcp, &tls, &[b"imap"])?;
let context = if starttls {
drive_capability(&mut stream, context)?
} else {
drive_greeting_with_capability(&mut stream, context)?
};
(context, stream)
}
scheme if scheme.eq_ignore_ascii_case("unix") => {
let sock_path = url.path();
let mut unix = UnixStream::connect(sock_path)?;
let context = drive_greeting_with_capability(&mut unix, context)?;
(context, Stream::Unix(unix))
}
scheme => {
bail!("Unknown scheme {scheme}, expected imap, imaps or unix");
}
};
if !context.authenticated {
let ir = context.capability.contains(&Capability::SaslIr);
let mechanism = sasl
.mechanism
.or(Some(SaslMechanism::Plain).filter(|_| sasl.plain.is_some()))
.or(Some(SaslMechanism::Login).filter(|_| sasl.login.is_some()));
match mechanism {
None => bail!("no SASL mechanism configured"),
Some(SaslMechanism::Login) => {
let Some(auth) = sasl.login.take() else {
bail!("missing SASL LOGIN configuration");
};
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut coroutine = ImapSessionLogin::new(
context,
ImapSessionLoginParams::new(auth.username, auth.password)?,
);
let mut arg: Option<&[u8]> = None;
context = loop {
match coroutine.resume(arg.take()) {
ImapSessionLoginResult::Ok(c) => break c,
ImapSessionLoginResult::WantsRead => {
let n = stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapSessionLoginResult::WantsWrite(bytes) => {
stream.write_all(&bytes)?;
arg = None;
}
ImapSessionLoginResult::Err { err, .. } => bail!(err),
}
};
}
Some(SaslMechanism::Plain) => {
let Some(auth) = sasl.plain.take() else {
bail!("missing SASL PLAIN configuration");
};
let mut buf = [0u8; READ_BUFFER_SIZE];
let mut coroutine = ImapSessionAuthenticatePlain::new(
context,
ImapSessionAuthenticatePlainParams::new(
auth.authzid,
auth.authcid,
auth.passwd,
ir,
),
);
let mut arg: Option<&[u8]> = None;
context = loop {
match coroutine.resume(arg.take()) {
ImapSessionAuthenticatePlainResult::Ok(c) => break c,
ImapSessionAuthenticatePlainResult::WantsRead => {
let n = stream.read(&mut buf)?;
arg = Some(&buf[..n]);
}
ImapSessionAuthenticatePlainResult::WantsWrite(bytes) => {
stream.write_all(&bytes)?;
arg = None;
}
ImapSessionAuthenticatePlainResult::Err { err, .. } => bail!(err),
}
};
}
Some(SaslMechanism::Anonymous) => {
unimplemented!("ANONYMOUS SASL mechanism not yet implemented")
}
}
}
Ok(Self { context, stream })
}
}