POP3
Old POP3 mailboxes read over POP3S or STLS: messages counted and listed with their UIDs, downloaded and read into sender, subject, text and attachments, or just their headers, and deleted when the session ends.
Version 1.0.0 · 8 functions · 3 crates
Functions
What use pop3.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use pop3_stat, pop3_list from pop3 (Quillo 0.6).
pop3_stat(url: str, opts: any) → anyor an errorHow many messages wait in the mailbox at url (pop3://user:password@host or pop3s://...) and their size in bytes: { count, size, tls }. Options: user, password (instead of the URL's), starttls ("if_offered", "required", "no"), ca, insecure, timeout_ms, allow_plain_password. The session stays open for the next call: pop3_quit ends it.
pop3_list(url: str, opts: any) → listor an errorEvery message's number, size and UID (UIDL): [{ number, size, uid }]. The UID stays the same across sessions, the number does not.
pop3_retr(url: str, id: any, opts: any) → anyor an errorA message (by number or UID) downloaded and read: { number, uid, subject, from, to, cc, date, message_id, text, html, attachments: [{ name, type, size }], size }.
pop3_raw(url: str, id: any, opts: any) → stror an errorA message as it was sent (RFC 5322 text), to keep or to read with another plugin.
pop3_top(url: str, id: any, lines: int, opts: any) → anyor an errorA message's headers and its first lines lines (TOP), read like pop3_retr without downloading it all.
pop3_dele(url: str, id: any, opts: any) → boolor an errorMarks a message (by number or UID) to be deleted when the session ends with pop3_quit; pop3_reset unmarks them all.
pop3_reset(url: str, opts: any) → boolor an errorUnmarks every message marked for deletion in this session (RSET).
pop3_quit(url: str) → boolor an errorEnds the session (QUIT): the messages marked are deleted now, and the next call sees new mail. Whether a session was open.
Get it
$ quillo install pop3 # pop3.qwer into this folder$ quillo install pop3 --example # and its example, pop3.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/pop3.qwer$ curl -O https://quillolang.org/plugins/files/pop3.qweThen, in your program, all of it or only what you call:
use pop3.qweruse pop3_stat, pop3_list from pop3 // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }webpki-roots = "1"mail-parser = "0.9"Example
// A support desk reading an old POP3 mailbox: what waits there, a customer's reply read (with// the line that starts with a dot kept), an invoice's headers and attachment, one message// deleted for good at QUIT, and a wrong password.//// POP3_URL=pop3s://desk:secret@mail.example.com quillo run pop3.qwe then open http://localhost:8080/inbox//// Needs a POP3 server at POP3_URL (pop3:// upgrades with STLS, pop3s:// is TLS from the start).// POP3_CA is a CA's PEM (text or file) for a server with a private certificate.use "pop3.qwer"let URL = env("POP3_URL", "pop3s://desk:secret@127.0.0.1")let OPTS = { starttls: "required", ca: env("POP3_CA", ""), timeout_ms: 5000 }route GET /inbox = { stat: pop3_stat(URL, OPTS)?, messages: pop3_list(URL, OPTS)?,}route GET /read { let m = pop3_retr(URL, "uid-101", OPTS)? { subject: m.subject, from: m.from.email, date: m.date, in_reply_to: m.in_reply_to, text: m.text }}route GET /invoice { let head = pop3_top(URL, 2, 0, OPTS)? let m = pop3_retr(URL, 2, OPTS)? { subject: head.subject, from: head.from.name, attachments: m.attachments, html: m.html != null }}// The newsletter goes: marked, then deleted when the session ends; the next session has one less.route GET /delete { let before = pop3_stat(URL, OPTS)?.count pop3_dele(URL, "uid-103", OPTS)? pop3_quit(URL)? { before, after: pop3_stat(URL, OPTS)?.count }}route GET /wrong = { error: pop3_stat(replace(URL, "s3cret", "guess"), OPTS).error }$ quillo run pop3.qwe$ curl 'localhost:8080/inbox'$ curl 'localhost:8080/read'$ curl 'localhost:8080/invoice'$ curl 'localhost:8080/delete'$ curl 'localhost:8080/wrong'It talks to the Redis that REDIS_URL names, else Redis on this machine. The site's test runs it against a stand-in.
Source
//! POP3 for old mailboxes and devices that only offer it: messages counted, listed with their UIDs, downloaded and read (subject, sender, date, text, HTML, attachments listed) or just their headers (TOP), marked for deletion and deleted at QUIT, over POP3S or STLS.//! version: 1.0.0//!//! [dependencies]//! rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }//! webpki-roots = "1"//! mail-parser = "0.9"use mail_parser::MimeHeaders;use serde_json::{json, Value};use std::collections::HashMap;use std::io::{Read, Write};use std::net::{SocketAddr, TcpStream, ToSocketAddrs};use std::sync::{Arc, Mutex};use std::time::Duration;// ---- Options, TCP with timeouts, and TLS by rustls (ring) ----fn o_str(o: &Value, k: &str) -> Option<String> { match o.get(k) { Some(Value::String(s)) => Some(s.clone()), Some(Value::Number(n)) => Some(n.to_string()), Some(Value::Bool(b)) => Some(b.to_string()), _ => None, }}fn o_i64(o: &Value, k: &str, d: i64) -> i64 { match o.get(k) { Some(Value::Number(n)) => n.as_i64().or(n.as_f64().map(|f| f as i64)).unwrap_or(d), Some(Value::String(s)) => s.trim().parse().unwrap_or(d), _ => d, }}fn o_bool(o: &Value, k: &str, d: bool) -> bool { match o.get(k) { Some(Value::Bool(b)) => *b, Some(Value::String(s)) => matches!(s.to_ascii_lowercase().as_str(), "true" | "yes" | "1" | "on"), Some(Value::Number(n)) => n.as_i64() != Some(0), _ => d, }}/// `timeout_ms` from the options (10 s when not given).fn timeout_of(o: &Value) -> Duration { Duration::from_millis(o_i64(o, "timeout_ms", 10_000).clamp(100, 600_000) as u64)}/// `host`, `host:port`, `[v6]:port` or a bare IPv6 address, with the protocol's port when none is given.fn split_host(host: &str, default_port: u16) -> Result<(String, u16), String> { let h = host.trim(); if h.is_empty() { return Err("no host given: write host or host:port".into()); } let (name, port) = if let Some(rest) = h.strip_prefix('[') { let end = rest.find(']').ok_or(format!("{}: a [ without its ]", host))?; let after = &rest[end + 1..]; (rest[..end].to_string(), after.strip_prefix(':').map(|p| p.to_string())) } else if h.matches(':').count() == 1 { let (a, b) = h.split_once(':').unwrap(); (a.to_string(), Some(b.to_string())) } else { (h.to_string(), None) }; let port = match port { Some(p) => p.parse::<u16>().map_err(|_| format!("{}: the port is not a number from 1 to 65535", host))?, None => default_port, }; Ok((name, port))}/// Connects with a time limit to each address the name has, and keeps the limit on reads and writes.fn connect(name: &str, port: u16, t: Duration) -> Result<TcpStream, String> { let addrs: Vec<SocketAddr> = (name, port).to_socket_addrs().map_err(|e| format!("{}: {}", name, e))?.collect(); if addrs.is_empty() { return Err(format!("{} has no address", name)); } let mut last = String::new(); for a in addrs { match TcpStream::connect_timeout(&a, t) { Ok(s) => { let _ = s.set_read_timeout(Some(t)); let _ = s.set_write_timeout(Some(t)); let _ = s.set_nodelay(true); return Ok(s); } Err(e) => last = format!("cannot reach {}: {}", a, e), } } Err(last)}/// Takes any certificate: for `insecure: true`, on a test bench only.#[derive(Debug)]struct AcceptAny(Arc<rustls::crypto::CryptoProvider>);impl rustls::client::danger::ServerCertVerifier for AcceptAny { fn verify_server_cert(&self, _: &rustls::pki_types::CertificateDer<'_>, _: &[rustls::pki_types::CertificateDer<'_>], _: &rustls::pki_types::ServerName<'_>, _: &[u8], _: rustls::pki_types::UnixTime) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> { Ok(rustls::client::danger::ServerCertVerified::assertion()) } fn verify_tls12_signature(&self, m: &[u8], c: &rustls::pki_types::CertificateDer<'_>, d: &rustls::DigitallySignedStruct) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> { rustls::crypto::verify_tls12_signature(m, c, d, &self.0.signature_verification_algorithms) } fn verify_tls13_signature(&self, m: &[u8], c: &rustls::pki_types::CertificateDer<'_>, d: &rustls::DigitallySignedStruct) -> Result<rustls::client::danger::HandshakeSignatureValid, rustls::Error> { rustls::crypto::verify_tls13_signature(m, c, d, &self.0.signature_verification_algorithms) } fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> { self.0.signature_verification_algorithms.supported_schemes() }}fn b64_decode(text: &str) -> Vec<u8> { let mut out = vec![]; let (mut acc, mut bits) = (0u32, 0); for c in text.bytes() { let v = match c { b'A'..=b'Z' => c - b'A', b'a'..=b'z' => c - b'a' + 26, b'0'..=b'9' => c - b'0' + 52, b'+' | b'-' => 62, b'/' | b'_' => 63, _ => continue, } as u32; acc = acc << 6 | v; bits += 6; if bits >= 8 { bits -= 8; out.push((acc >> bits) as u8); } } out}/// The certificates in PEM text.fn pem_certs(text: &str) -> Vec<Vec<u8>> { let mut out = vec![]; let mut rest = text; while let Some(a) = rest.find("-----BEGIN CERTIFICATE-----") { let body = &rest[a + 27..]; let Some(b) = body.find("-----END CERTIFICATE-----") else { break }; out.push(b64_decode(&body[..b])); rest = &body[b + 25..]; } out}/// A rustls client set up from the options: Mozilla's roots, plus `ca` (PEM text, or a PEM file's path); `insecure: true` takes any certificate.fn tls_config(o: &Value) -> Result<Arc<rustls::ClientConfig>, String> { let provider = Arc::new(rustls::crypto::ring::default_provider()); let builder = rustls::ClientConfig::builder_with_provider(provider.clone()).with_safe_default_protocol_versions().map_err(|e| e.to_string())?; let config = if o_bool(o, "insecure", false) { builder.dangerous().with_custom_certificate_verifier(Arc::new(AcceptAny(provider))).with_no_client_auth() } else { let mut roots = rustls::RootCertStore { roots: webpki_roots::TLS_SERVER_ROOTS.to_vec() }; if let Some(ca) = o_str(o, "ca").filter(|s| !s.trim().is_empty()) { let text = if ca.contains("-----BEGIN") { ca.clone() } else { std::fs::read_to_string(&ca).map_err(|e| format!("cannot read the ca file {}: {}", ca, e))? }; let certs = pem_certs(&text); if certs.is_empty() { return Err("`ca` holds no PEM certificate (-----BEGIN CERTIFICATE-----)".into()); } for c in certs { roots.add(rustls::pki_types::CertificateDer::from(c)).map_err(|e| format!("a `ca` certificate cannot be used: {}", e))?; } } builder.with_root_certificates(roots).with_no_client_auth() }; Ok(Arc::new(config))}enum Stream { Plain(TcpStream), Tls(Box<rustls::StreamOwned<rustls::ClientConnection, TcpStream>>),}impl Stream { fn tcp(&self) -> &TcpStream { match self { Stream::Plain(s) => s, Stream::Tls(s) => &s.sock, } }}impl Read for Stream { fn read(&mut self, b: &mut [u8]) -> std::io::Result<usize> { match self { Stream::Plain(s) => s.read(b), Stream::Tls(s) => match s.read(b) { Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => Ok(0), r => r, }, } }}impl Write for Stream { fn write(&mut self, b: &[u8]) -> std::io::Result<usize> { match self { Stream::Plain(s) => s.write(b), Stream::Tls(s) => s.write(b), } } fn flush(&mut self) -> std::io::Result<()> { match self { Stream::Plain(s) => s.flush(), Stream::Tls(s) => s.flush(), } }}/// TLS over a connected socket, the handshake done now so a bad certificate is said at once. `server_name` in the options overrides the name checked.fn tls_wrap(mut tcp: TcpStream, name: &str, o: &Value) -> Result<Stream, String> { let config = tls_config(o)?; let sni = o_str(o, "server_name").unwrap_or_else(|| name.to_string()); let server = rustls::pki_types::ServerName::try_from(sni.clone()).map_err(|_| format!("{} is not a name TLS can check", sni))?; let mut conn = rustls::ClientConnection::new(config, server).map_err(|e| e.to_string())?; while conn.is_handshaking() { conn.complete_io(&mut tcp).map_err(|e| format!("TLS with {} failed: {} (a private CA? pass its PEM as `ca`)", name, e))?; } Ok(Stream::Tls(Box::new(rustls::StreamOwned::new(conn, tcp))))}/// A stream with a read buffer, for protocols of lines.struct Lines { s: Stream, buf: Vec<u8>,}impl Lines { fn new(s: Stream) -> Lines { Lines { s, buf: vec![] } } fn fill(&mut self) -> Result<(), String> { let mut b = [0u8; 16384]; match self.s.read(&mut b) { Ok(0) => Err("the server closed the connection".into()), Ok(n) => { self.buf.extend_from_slice(&b[..n]); Ok(()) } Err(e) if matches!(e.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut) => Err("the server did not answer in time (timeout_ms)".into()), Err(e) => Err(e.to_string()), } } /// One line, without its CRLF. fn line(&mut self) -> Result<Vec<u8>, String> { loop { if let Some(i) = self.buf.iter().position(|&c| c == b'\n') { let mut l: Vec<u8> = self.buf.drain(..=i).collect(); l.pop(); if l.last() == Some(&b'\r') { l.pop(); } return Ok(l); } if self.buf.len() > 16 << 20 { return Err("a line longer than 16 MB".into()); } self.fill()?; } } fn exact(&mut self, n: usize) -> Result<Vec<u8>, String> { while self.buf.len() < n { self.fill()?; } Ok(self.buf.drain(..n).collect()) } fn send(&mut self, b: &[u8]) -> Result<(), String> { self.s.write_all(b).and_then(|_| self.s.flush()).map_err(|e| format!("cannot write to the server: {}", e)) }}// ---- Accounts and messages ----struct Account { tls: bool, host: String, port: u16, user: String, password: String, path: String,}fn pct_decode(s: &str) -> String { let b = s.as_bytes(); let hexv = |c: u8| (c as char).to_digit(16).map(|d| d as u8); let mut out = vec![]; let mut i = 0; while i < b.len() { if b[i] == b'%' && i + 2 < b.len() { if let (Some(h), Some(l)) = (hexv(b[i + 1]), hexv(b[i + 2])) { out.push(h << 4 | l); i += 3; continue; } } out.push(b[i]); i += 1; } String::from_utf8_lossy(&out).to_string()}/// `scheme://user:password@host:port/path`; `user` and `password` in the options win over the URL's (no percent-encoding needed there).fn account(url: &str, opts: &Value, plain: &str, secure: &str, plain_port: u16, secure_port: u16) -> Result<Account, String> { let (scheme, rest) = url.split_once("://").ok_or(format!("{} is not a URL: write {}://user:password@host", url, plain))?; let tls = match scheme.to_ascii_lowercase().as_str() { s if s == plain => false, s if s == secure => true, s => return Err(format!("{}:// is not {}:// or {}://", s, plain, secure)), }; let (auth_host, path) = match rest.find('/') { Some(i) => (&rest[..i], rest[i + 1..].to_string()), None => (rest, String::new()), }; let (auth, hostport) = match auth_host.rfind('@') { Some(i) => (&auth_host[..i], &auth_host[i + 1..]), None => ("", auth_host), }; let (u, p) = auth.split_once(':').unwrap_or((auth, "")); let (host, port) = split_host(hostport, if tls { secure_port } else { plain_port })?; let user = o_str(opts, "user").unwrap_or_else(|| pct_decode(u)); let password = o_str(opts, "password").unwrap_or_else(|| pct_decode(p)); if user.is_empty() { return Err(format!("no user name: write {}://user:password@{} or give `user` and `password`", plain, hostport)); } Ok(Account { tls, host, port, user, password, path: pct_decode(&path) })}fn people(a: Option<&mail_parser::Address>) -> Vec<Value> { a.map(|a| a.iter().map(|p| json!({ "name": p.name(), "email": p.address() })).collect()).unwrap_or_default()}/// A message read with mail-parser: who, when, what, the text, the HTML (only when there is an HTML part) and the attachments listed.fn summary(raw: &[u8]) -> Value { let Some(m) = mail_parser::MessageParser::default().parse(raw) else { return json!({ "error": "not an email", "size": raw.len() }); }; let has_html = m.parts.iter().any(|p| matches!(p.body, mail_parser::PartType::Html(_))); let attachments: Vec<Value> = m .attachments() .map(|a| { json!({ "name": a.attachment_name(), "type": a.content_type().map(|c| format!("{}/{}", c.c_type, c.c_subtype.as_deref().unwrap_or("octet-stream"))), "size": a.contents().len(), }) }) .collect(); json!({ "subject": m.subject(), "from": people(m.from()).into_iter().next(), "to": people(m.to()), "cc": people(m.cc()), "reply_to": people(m.reply_to()), "date": m.date().map(|d| d.to_rfc3339()), "message_id": m.message_id(), "in_reply_to": m.in_reply_to().as_text(), "text": m.body_text(0).map(|t| t.into_owned()), "html": if has_html { m.body_html(0).map(|t| t.into_owned()) } else { None }, "attachments": attachments, "size": raw.len(), })}// ---- POP3 ----struct Box3 { c: Lines, tls: bool, uids: Option<Vec<(i64, String)>>,}static BOXES: Mutex<Option<HashMap<String, Box3>>> = Mutex::new(None);/// One call at a time on a mailbox: POP3 servers lock it for one session.static TURNS: Mutex<Option<HashMap<String, Arc<Mutex<()>>>>> = Mutex::new(None);fn turn(key: &str) -> Arc<Mutex<()>> { TURNS.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(HashMap::new).entry(key.to_string()).or_default().clone()}fn ok(line: &[u8]) -> Result<String, String> { let l = String::from_utf8_lossy(line).to_string(); if let Some(rest) = l.strip_prefix("+OK") { Ok(rest.trim().to_string()) } else { Err(format!("the server said: {}", l.trim_start_matches("-ERR").trim())) }}fn command(c: &mut Lines, cmd: &str) -> Result<String, String> { c.send(format!("{}\r\n", cmd).as_bytes())?; ok(&c.line()?)}/// The lines of a multi-line answer, up to the lone ".", with dot-stuffing taken off.fn multi(c: &mut Lines) -> Result<Vec<Vec<u8>>, String> { let mut out = vec![]; loop { let l = c.line()?; if l == b"." { return Ok(out); } out.push(if l.starts_with(b"..") { l[1..].to_vec() } else { l }); }}fn login(url: &str, opts: &Value) -> Result<Box3, String> { let a = account(url, opts, "pop3", "pop3s", 110, 995)?; let t = timeout_of(opts); let tcp = connect(&a.host, a.port, t)?; let mut c = Lines::new(if a.tls { tls_wrap(tcp, &a.host, opts)? } else { Stream::Plain(tcp) }); ok(&c.line()?).map_err(|e| format!("{} did not greet as POP3 does: {}", a.host, e))?; let mut tls = a.tls; let starttls = o_str(opts, "starttls").unwrap_or_else(|| "if_offered".into()); if !tls && starttls != "no" && starttls != "false" { let caps: Vec<String> = match command(&mut c, "CAPA") { Ok(_) => multi(&mut c)?.iter().map(|l| String::from_utf8_lossy(l).to_ascii_uppercase()).collect(), Err(_) => vec![], }; if caps.iter().any(|l| l.trim() == "STLS") { command(&mut c, "STLS")?; let Lines { s, .. } = c; let Stream::Plain(tcp) = s else { unreachable!() }; c = Lines::new(tls_wrap(tcp, &a.host, opts)?); tls = true; } else if starttls == "required" || starttls == "true" || starttls == "yes" { return Err(format!("{} does not offer STLS, and starttls is required", a.host)); } } if !tls && !o_bool(opts, "allow_plain_password", a.host == "127.0.0.1" || a.host == "localhost" || a.host == "::1") { return Err(format!("{} offers no TLS: the password would go in clear. Use pop3s://, or allow_plain_password: true", a.host)); } command(&mut c, &format!("USER {}", a.user)).map_err(|e| format!("USER refused: {}", e))?; command(&mut c, &format!("PASS {}", a.password)).map_err(|e| format!("sign-in failed: {}", e))?; Ok(Box3 { c, tls, uids: None })}fn with<T>(url: &str, opts: &Value, f: impl Fn(&mut Box3) -> Result<T, String>) -> Result<T, String> { let key = url.to_string(); let t = turn(&key); let _turn = t.lock().unwrap_or_else(|e| e.into_inner()); let kept = BOXES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(HashMap::new).remove(&key); let mut b = match kept { Some(mut b) => { let _ = b.c.s.tcp().set_read_timeout(Some(timeout_of(opts))); // A kept session the server dropped (POP3 servers time out idle ones) is opened again. if command(&mut b.c, "NOOP").is_ok() { b } else { login(url, opts)? } } None => login(url, opts)?, }; let r = f(&mut b); let broken = matches!(&r, Err(e) if !e.starts_with("the server said")); if !broken { BOXES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(HashMap::new).insert(key, b); } r}fn uids(b: &mut Box3) -> Result<Vec<(i64, String)>, String> { if let Some(u) = &b.uids { return Ok(u.clone()); } command(&mut b.c, "UIDL")?; let list: Vec<(i64, String)> = multi(&mut b.c)? .iter() .filter_map(|l| { let s = String::from_utf8_lossy(l); let mut it = s.split_whitespace(); Some((it.next()?.parse().ok()?, it.next()?.to_string())) }) .collect(); b.uids = Some(list.clone()); Ok(list)}/// A message number from a number, or from a UIDL ID (text).fn number(b: &mut Box3, id: &Value) -> Result<i64, String> { match id { Value::Number(n) => n.as_i64().filter(|n| *n > 0).ok_or("message numbers start at 1".to_string()), Value::String(s) => { if let Ok(n) = s.parse::<i64>() { if uids(b).map_or(true, |u| !u.iter().any(|x| &x.1 == s)) { return Ok(n); } } uids(b)?.iter().find(|x| &x.1 == s).map(|x| x.0).ok_or(format!("no message has the UID {} (deleted, or seen before this session?)", s)) } _ => Err("give a message number or its UID".into()), }}/// How many messages wait in the mailbox at `url` (pop3://user:password@host or pop3s://...) and their size in bytes: `{ count, size, tls }`. Options: `user`, `password` (instead of the URL's), `starttls` ("if_offered", "required", "no"), `ca`, `insecure`, `timeout_ms`, `allow_plain_password`. The session stays open for the next call: pop3_quit ends it.pub fn pop3_stat(url: &str, opts: serde_json::Value) -> Result<Value, String> { with(url, &opts, |b| { let r = command(&mut b.c, "STAT")?; let mut it = r.split_whitespace(); let count: i64 = it.next().and_then(|x| x.parse().ok()).unwrap_or(0); let size: i64 = it.next().and_then(|x| x.parse().ok()).unwrap_or(0); Ok(json!({ "count": count, "size": size, "tls": b.tls })) })}/// Every message's number, size and UID (UIDL): `[{ number, size, uid }]`. The UID stays the same across sessions, the number does not.pub fn pop3_list(url: &str, opts: serde_json::Value) -> Result<Vec<Value>, String> { with(url, &opts, |b| { command(&mut b.c, "LIST")?; let sizes: Vec<(i64, i64)> = multi(&mut b.c)? .iter() .filter_map(|l| { let s = String::from_utf8_lossy(l); let mut it = s.split_whitespace(); Some((it.next()?.parse().ok()?, it.next()?.parse().ok()?)) }) .collect(); let u = uids(b).unwrap_or_default(); Ok(sizes.iter().map(|(n, size)| json!({ "number": n, "size": size, "uid": u.iter().find(|x| x.0 == *n).map(|x| x.1.clone()) })).collect()) })}/// A message (by number or UID) downloaded and read: `{ number, uid, subject, from, to, cc, date, message_id, text, html, attachments: [{ name, type, size }], size }`.pub fn pop3_retr(url: &str, id: serde_json::Value, opts: serde_json::Value) -> Result<Value, String> { with(url, &opts, |b| { let n = number(b, &id)?; command(&mut b.c, &format!("RETR {}", n))?; let raw = multi(&mut b.c)?.join(&b"\r\n"[..]); let mut m = summary(&raw); m["number"] = json!(n); m["uid"] = json!(uids(b).ok().and_then(|u| u.iter().find(|x| x.0 == n).map(|x| x.1.clone()))); Ok(m) })}/// A message as it was sent (RFC 5322 text), to keep or to read with another plugin.pub fn pop3_raw(url: &str, id: serde_json::Value, opts: serde_json::Value) -> Result<String, String> { with(url, &opts, |b| { let n = number(b, &id)?; command(&mut b.c, &format!("RETR {}", n))?; Ok(String::from_utf8_lossy(&multi(&mut b.c)?.join(&b"\r\n"[..])).to_string()) })}/// A message's headers and its first `lines` lines (TOP), read like pop3_retr without downloading it all.pub fn pop3_top(url: &str, id: serde_json::Value, lines: i64, opts: serde_json::Value) -> Result<Value, String> { with(url, &opts, |b| { let n = number(b, &id)?; command(&mut b.c, &format!("TOP {} {}", n, lines.max(0)))?; let raw = multi(&mut b.c)?.join(&b"\r\n"[..]); let mut m = summary(&raw); m["number"] = json!(n); Ok(m) })}/// Marks a message (by number or UID) to be deleted when the session ends with pop3_quit; pop3_reset unmarks them all.pub fn pop3_dele(url: &str, id: serde_json::Value, opts: serde_json::Value) -> Result<bool, String> { with(url, &opts, |b| { let n = number(b, &id)?; command(&mut b.c, &format!("DELE {}", n))?; Ok(true) })}/// Unmarks every message marked for deletion in this session (RSET).pub fn pop3_reset(url: &str, opts: serde_json::Value) -> Result<bool, String> { with(url, &opts, |b| { command(&mut b.c, "RSET")?; Ok(true) })}/// Ends the session (QUIT): the messages marked are deleted now, and the next call sees new mail. Whether a session was open.pub fn pop3_quit(url: &str) -> Result<bool, String> { let t = turn(url); let _turn = t.lock().unwrap_or_else(|e| e.into_inner()); let kept = BOXES.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(HashMap::new).remove(url); match kept { Some(mut b) => { command(&mut b.c, "QUIT").map_err(|e| format!("QUIT failed, so the deletions may not be done: {}", e))?; Ok(true) } None => Ok(false), }}Related
imap.qwer · 1.0.0Inboxes read and filed over IMAP: folders listed, messages searched by unseen, date or sender, fetched and read with their attachments listed, flagged, moved, appended to Sent, and new mail waited for with IDLE.7 views · 7 downloadsFTP and FTPS ftp.qwer · 1.0.0Files exchanged with FTP and FTPS servers, as partners and older machines still offer them: folders listed, files uploaded and downloaded, renamed, deleted and filed into new folders, in passive mode over explicit or implicit TLS.7 views · 8 downloadsEmail reader email_parse.qwer · 1.0.0Read raw emails: sender, recipients, subject, date, the text and HTML bodies and the attachments, for support inboxes and replies by email.8 views · 6 downloadsMailboxes mbox.qwer · 1.0.0Mailboxes in the mbox format read message by message: lists, search, one message in full with its attachments, each saved as .eml, and messages appended.7 views · 9 downloadsOutlook messages outlook_msg.qwer · 1.0.0Outlook .msg emails read: sender, recipients, subject, date, the text, HTML and RTF bodies, the attachments saved, and the message turned into a standard .eml.6 views · 7 downloadsDNS blocklists dnsbl.qwer · 1.0.0Ask several IP and domain blocklists at once, addresses reversed by octet or IPv6 nibble, with return codes named, TXT reasons read, and a list's refusal, failure or silence told apart from a listing.4 views · 8 downloadsThread 0 posts
On POP3: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.