Browse Source

melib/nntp: implement NNTP posting

master
Manos Pitsidianakis 3 months ago
parent
commit
521f634e7b
Signed by: epilys GPG Key ID: 73627C2F690DF710
  1. 50
      docs/meli.conf.5
  2. 1
      melib/build.rs
  3. 9
      melib/src/backends.rs
  4. 69
      melib/src/backends/imap/connection.rs
  5. 40
      melib/src/backends/nntp.rs
  6. 47
      melib/src/backends/nntp/connection.rs
  7. 1
      melib/src/text_processing/line_break.rs
  8. 10
      melib/src/text_processing/tables.rs
  9. 3
      src/components/mail/compose.rs
  10. 34
      src/conf/accounts.rs
  11. 40
      src/conf/composing.rs

50
docs/meli.conf.5

@ -346,6 +346,56 @@ Example:
format = "mbox"
mailboxes."Python mailing list" = { path = "~/.mail/python.mbox", subscribe = true, autoload = true }
.Ed
.Ss NNTP
NNTP specific options
.Bl -tag -width 36n
.It Ic server_hostname Ar String
example:
.Qq nntp.example.com
.It Ic server_username Ar String
Server username
.It Ic server_password Ar String
Server password
.It Ic require_auth Ar bool
.Pq Em optional
require authentication in every case
.\" default value
.Pq Em true
.It Ic use_tls Ar boolean
.Pq Em optional
Connect with TLS.
.\" default value
.Pq Em false
.It Ic server_port Ar number
.Pq Em optional
The port to connect to
.\" default value
.Pq Em 119
.It Ic danger_accept_invalid_certs Ar boolean
.Pq Em optional
Do not validate TLS certificates.
.\" default value
.Pq Em false
.El
.Pp
You have to explicitly state the groups you want to see in the
.Ic mailboxes
field.
Example:
.Bd -literal
[accounts.sicpm.mailboxes]
"sic.all" = {}
.Ed
.Pp
To submit articles directly to the NNTP server, you must set the special value
.Em server_submission
in the
.Ic send_mail
field.
Example:
.Bd -literal
composing.send_mail = "server_submission"
.Ed
.Ss MAILBOXES
.Bl -tag -width 36n
.It Ic alias Ar String

1
melib/build.rs

@ -43,7 +43,6 @@ fn main() -> Result<(), std::io::Error> {
const EMOJI_DATA_URL: &str =
"https://www.unicode.org/Public/UCD/latest/ucd/emoji/emoji-data.txt";
let mod_path = Path::new(MOD_PATH);
if mod_path.exists() {
eprintln!(

9
melib/src/backends.rs

@ -401,6 +401,15 @@ pub trait MailBackend: ::std::fmt::Debug + Send + Sync {
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
Err(MeliError::new("Unimplemented."))
}
fn submit(
&self,
bytes: Vec<u8>,
mailbox_hash: Option<MailboxHash>,
flags: Option<Flag>,
) -> ResultFuture<()> {
Err(MeliError::new("Not supported in this backend."))
}
}
/// A `BackendOp` manages common operations for the various mail backends. They only live for the

69
melib/src/backends/imap/connection.rs

@ -588,7 +588,9 @@ impl ImapConnection {
pub fn connect<'a>(&'a mut self) -> Pin<Box<dyn Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
if let (time, ref mut status @ Ok(())) = *self.uid_store.is_online.lock().unwrap() {
if SystemTime::now().duration_since(time).unwrap_or_default() >= IMAP_PROTOCOL_TIMEOUT {
if SystemTime::now().duration_since(time).unwrap_or_default()
>= IMAP_PROTOCOL_TIMEOUT
{
let err = MeliError::new("Connection timed out").set_kind(ErrorKind::Timeout);
*status = Err(err.clone());
self.stream = Err(err);
@ -978,45 +980,38 @@ impl ImapConnection {
pub async fn unselect(&mut self) -> Result<()> {
match self.stream.as_mut()?.current_mailbox.take() {
MailboxSelection::Examine(_) |
MailboxSelection::Select(_) => {
let mut response = Vec::with_capacity(8 * 1024);
if self
.uid_store
.capabilities
.lock()
.unwrap()
.iter()
.any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT"))
MailboxSelection::Examine(_) | MailboxSelection::Select(_) => {
let mut response = Vec::with_capacity(8 * 1024);
if self
.uid_store
.capabilities
.lock()
.unwrap()
.iter()
.any(|cap| cap.eq_ignore_ascii_case(b"UNSELECT"))
{
self.send_command(b"UNSELECT").await?;
self.read_response(&mut response, RequiredResponses::empty())
.await?;
} else {
/* `RFC3691 - UNSELECT Command` states: "[..] IMAP4 provides this
* functionality (via a SELECT command with a nonexistent mailbox name or
* reselecting the same mailbox with EXAMINE command)[..]
*/
let mut nonexistent = "blurdybloop".to_string();
{
self.send_command(b"UNSELECT").await?;
self.read_response(&mut response, RequiredResponses::empty())
.await?;
} else {
/* `RFC3691 - UNSELECT Command` states: "[..] IMAP4 provides this
* functionality (via a SELECT command with a nonexistent mailbox name or
* reselecting the same mailbox with EXAMINE command)[..]
*/
let mut nonexistent = "blurdybloop".to_string();
{
let mailboxes = self.uid_store.mailboxes.lock().await;
while mailboxes.values().any(|m| m.imap_path() == nonexistent) {
nonexistent.push('p');
}
}
self.send_command(
format!(
"SELECT \"{}\"",
nonexistent
)
.as_bytes(),
)
.await?;
self.read_response(&mut response, RequiredResponses::NO_REQUIRED)
.await?;
let mailboxes = self.uid_store.mailboxes.lock().await;
while mailboxes.values().any(|m| m.imap_path() == nonexistent) {
nonexistent.push('p');
}
}
self.send_command(format!("SELECT \"{}\"", nonexistent).as_bytes())
.await?;
self.read_response(&mut response, RequiredResponses::NO_REQUIRED)
.await?;
}
MailboxSelection::None => {},
}
MailboxSelection::None => {}
}
Ok(())
}

40
melib/src/backends/nntp.rs

@ -131,6 +131,7 @@ impl MailBackend for NntpType {
)
})
.collect::<Vec<(String, MailBackendExtensionStatus)>>();
let mut supports_submission = false;
let NntpExtensionUse {
#[cfg(feature = "deflate_compression")]
deflate,
@ -138,6 +139,10 @@ impl MailBackend for NntpType {
{
for (name, status) in extensions.iter_mut() {
match name.as_str() {
s if s.eq_ignore_ascii_case("POST") => {
supports_submission = true;
*status = MailBackendExtensionStatus::Enabled { comment: None };
}
"COMPRESS DEFLATE" => {
#[cfg(feature = "deflate_compression")]
{
@ -171,7 +176,7 @@ impl MailBackend for NntpType {
supports_search: false,
extensions: Some(extensions),
supports_tags: false,
supports_submission: false,
supports_submission,
}
}
@ -354,6 +359,39 @@ impl MailBackend for NntpType {
) -> ResultFuture<SmallVec<[EnvelopeHash; 512]>> {
Err(MeliError::new("Unimplemented."))
}
fn submit(
&self,
bytes: Vec<u8>,
mailbox_hash: Option<MailboxHash>,
flags: Option<Flag>,
) -> ResultFuture<()> {
let connection = self.connection.clone();
Ok(Box::pin(async move {
match timeout(Some(Duration::from_secs(60 * 16)), connection.lock()).await {
Ok(mut conn) => {
match &conn.stream {
Ok(stream) => {
if !stream.supports_submission {
return Err(MeliError::new("Server prohibits posting."));
}
}
Err(err) => return Err(err.clone()),
}
let mut res = String::with_capacity(8 * 1024);
if let Some(mailbox_hash) = mailbox_hash {
conn.select_group(mailbox_hash, false, &mut res).await?;
}
conn.send_command(b"POST").await?;
conn.read_response(&mut res, false, &["340 "]).await?;
conn.send_multiline_data_block(&bytes).await?;
conn.read_response(&mut res, false, &["240 "]).await?;
Ok(())
}
Err(err) => Err(err),
}
}))
}
}
impl NntpType {

47
melib/src/backends/nntp/connection.rs

@ -45,7 +45,7 @@ impl Default for NntpExtensionUse {
fn default() -> Self {
Self {
#[cfg(feature = "deflate_compression")]
deflate: true,
deflate: false,
}
}
}
@ -55,6 +55,7 @@ pub struct NntpStream {
pub stream: AsyncWrapper<Connection>,
pub extension_use: NntpExtensionUse,
pub current_mailbox: MailboxSelection,
pub supports_submission: bool,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
@ -100,6 +101,7 @@ impl NntpStream {
stream,
extension_use: server_conf.extension_use,
current_mailbox: MailboxSelection::None,
supports_submission: false,
};
if server_conf.use_tls {
@ -114,6 +116,8 @@ impl NntpStream {
if server_conf.use_starttls {
ret.read_response(&mut res, false, &["200 ", "201 "])
.await?;
ret.supports_submission = res.starts_with("200");
ret.send_command(b"CAPABILITIES").await?;
ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES"))
.await?;
@ -193,9 +197,6 @@ impl NntpStream {
.chain_err_summary(|| format!("Could not initiate TLS negotiation to {}.", path))
.chain_err_kind(crate::error::ErrorKind::Network)?;
}
} else {
ret.read_response(&mut res, false, &["200 ", "201 "])
.await?;
}
//ret.send_command(
// format!(
@ -216,6 +217,9 @@ impl NntpStream {
);
}
ret.read_response(&mut res, false, &["200 ", "201 "])
.await?;
ret.supports_submission = res.starts_with("200");
ret.send_command(b"CAPABILITIES").await?;
ret.read_response(&mut res, true, command_to_replycodes("CAPABILITIES"))
.await?;
@ -225,7 +229,8 @@ impl NntpStream {
&server_conf.server_hostname, res
)));
}
let capabilities: HashSet<String> = res.lines().skip(1).map(|l| l.to_string()).collect();
let capabilities: HashSet<String> =
res.lines().skip(1).map(|l| l.trim().to_string()).collect();
if !capabilities
.iter()
.any(|cap| cap.eq_ignore_ascii_case("VERSION 2"))
@ -235,6 +240,12 @@ impl NntpStream {
&server_conf.server_hostname
)));
}
if !capabilities
.iter()
.any(|cap| cap.eq_ignore_ascii_case("POST"))
{
ret.supports_submission = false;
}
if server_conf.require_auth {
if capabilities.iter().any(|c| c.starts_with("AUTHINFO USER")) {
@ -280,6 +291,7 @@ impl NntpStream {
stream,
extension_use,
current_mailbox,
supports_submission,
} = ret;
let stream = stream.into_inner()?;
return Ok((
@ -288,6 +300,7 @@ impl NntpStream {
stream: AsyncWrapper::new(stream.deflate())?,
extension_use,
current_mailbox,
supports_submission,
},
));
}
@ -364,6 +377,9 @@ impl NntpStream {
}
pub async fn send_command(&mut self, command: &[u8]) -> Result<()> {
debug!("sending: {}", unsafe {
std::str::from_utf8_unchecked(command)
});
if let Err(err) = try_await(async move {
let command = command.trim();
self.stream.write_all(command).await?;
@ -383,13 +399,24 @@ impl NntpStream {
}
}
pub async fn send_multiline_data_block(&mut self, data: &str) -> Result<()> {
pub async fn send_multiline_data_block(&mut self, data: &[u8]) -> Result<()> {
if let Err(err) = try_await(async move {
for l in data.lines() {
if l.starts_with('.') {
let mut ptr = 0;
while let Some(pos) = data[ptr..].find("\n") {
let l = &data[ptr..ptr + pos].trim_end();
if l.starts_with(b".") {
self.stream.write_all(b".").await?;
}
self.stream.write_all(l).await?;
self.stream.write_all(b"\r\n").await?;
ptr += pos + 1;
}
let l = &data[ptr..].trim_end();
if !l.is_empty() {
if l.starts_with(b".") {
self.stream.write_all(b".").await?;
}
self.stream.write_all(l.as_bytes()).await?;
self.stream.write_all(l).await?;
self.stream.write_all(b"\r\n").await?;
}
self.stream.write_all(b".\r\n").await?;
@ -523,7 +550,7 @@ impl NntpConnection {
Ok(())
}
pub async fn send_multiline_data_block(&mut self, message: &str) -> Result<()> {
pub async fn send_multiline_data_block(&mut self, message: &[u8]) -> Result<()> {
self.stream
.as_mut()?
.send_multiline_data_block(message)

1
melib/src/text_processing/line_break.rs

@ -1246,7 +1246,6 @@ easy to take MORE than nothing.'"#;
}
}
mod segment_tree {
/*! Simple segment tree implementation for maximum in range queries. This is useful if given an
* array of numbers you want to get the maximum value inside an interval quickly.

10
melib/src/text_processing/tables.rs

@ -3455,15 +3455,9 @@ pub const LINE_BREAK_RULES: &[(u32, u32, LineBreakClass)] = &[
(0x100000, 0x10FFFD, XX),
];
pub const ASCII: &[(u32, u32)] = &[
(0x20, 0x7E),
];
pub const ASCII: &[(u32, u32)] = &[(0x20, 0x7E)];
pub const PRIVATE: &[(u32, u32)] = &[
(0xE000, 0xF8FF),
(0xF0000, 0xFFFFD),
(0x100000, 0x10FFFD),
];
pub const PRIVATE: &[(u32, u32)] = &[(0xE000, 0xF8FF), (0xF0000, 0xFFFFD), (0x100000, 0x10FFFD)];
pub const NONPRINT: &[(u32, u32)] = &[
(0x0, 0x1F),

3
src/components/mail/compose.rs

@ -477,6 +477,9 @@ impl Composer {
hostname.truncate_at_boundary(10);
format!("{} [smtp: {}]", acc.name(), hostname)
}
crate::conf::composing::SendMail::ServerSubmission => {
format!("{} [server submission]", acc.name())
}
};
(addr, desc)

34
src/conf/accounts.rs

@ -1326,6 +1326,25 @@ impl Account {
}
Ok(Some(handle))
}
SendMail::ServerSubmission => {
if self.backend_capabilities.supports_submission {
let job = self.backend.write().unwrap().submit(
message.clone().into_bytes(),
None,
None,
)?;
let handle = if self.backend_capabilities.is_async {
self.job_executor.spawn_specialized(job)
} else {
self.job_executor.spawn_blocking(job)
};
self.insert_job(handle.job_id, JobRequest::SendMessageBackground { handle });
return Ok(None);
}
return Err(MeliError::new("Server does not support submission.")
.set_summary("Message not sent."));
}
}
}
@ -1333,6 +1352,8 @@ impl Account {
&self,
send_mail: crate::conf::composing::SendMail,
) -> impl FnOnce(Arc<String>) -> Pin<Box<dyn Future<Output = Result<()>> + Send>> + Send {
let capabilities = self.backend_capabilities.clone();
let backend = self.backend.clone();
|message: Arc<String>| -> Pin<Box<dyn Future<Output = Result<()>> + Send>> {
Box::pin(async move {
use crate::conf::composing::SendMail;
@ -1386,6 +1407,19 @@ impl Account {
.mail_transaction(message.as_str(), None)
.await
}
SendMail::ServerSubmission => {
if capabilities.supports_submission {
let fut = backend.write().unwrap().submit(
message.as_bytes().to_vec(),
None,
None,
)?;
fut.await?;
return Ok(());
}
return Err(MeliError::new("Server does not support submission.")
.set_summary("Message not sent."));
}
}
})
}

40
src/conf/composing.rs

@ -90,10 +90,50 @@ impl Default for ComposingSettings {
}
}
macro_rules! named_unit_variant {
($variant:ident) => {
pub mod $variant {
pub fn serialize<S>(serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(stringify!($variant))
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<(), D::Error>
where
D: serde::Deserializer<'de>,
{
struct V;
impl<'de> serde::de::Visitor<'de> for V {
type Value = ();
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
f.write_str(concat!("\"", stringify!($variant), "\""))
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
if value == stringify!($variant) {
Ok(())
} else {
Err(E::invalid_value(serde::de::Unexpected::Str(value), &self))
}
}
}
deserializer.deserialize_str(V)
}
}
};
}
mod strings {
named_unit_variant!(server_submission);
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(untagged)]
pub enum SendMail {
#[cfg(feature = "smtp")]
Smtp(melib::smtp::SmtpServerConf),
#[serde(with = "strings::server_submission")]
ServerSubmission,
ShellCommand(String),
}
Loading…
Cancel
Save