melib/nntp: implement NNTP posting
parent
978939d8e3
commit
521f634e7b
|
@ -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
|
||||
|
|
|
@ -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!(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.as_bytes()).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).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)
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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…
Reference in New Issue