Protocols

IMAP

Inboxes 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.

Version 1.0.0 · 10 functions · 3 crates

#email#support#attachments#network

Functions

What use imap.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use imap_folders, imap_status from imap (Quillo 0.6).

imap_folders(url: str, opts: any) → listor an error

The folders of the account at url (imaps://user:password@host, or imap:// upgraded with STARTTLS): [{ name, delimiter, flags, special }], special being "sent", "trash", "drafts", "junk", "archive" or "all" where the server says (RFC 6154). Options: user, password, starttls ("if_offered", "required", "no"), ca, insecure, timeout_ms, allow_plain_password. The session stays open for the next call.

imap_status(url: str, folder: str, opts: any) → anyor an error

How many messages folder holds, how many are unseen, and its UIDNEXT and UIDVALIDITY (STATUS).

imap_fetch(url: str, folder: str, uids: any, opts: any) → listor an error

Messages from folder by UID (one, a list, or a set such as "100:*"), read: [{ uid, flags, seen, size, internal_date, subject, from, to, cc, date, message_id, in_reply_to, text, html, attachments: [{ name, type, size }] }]. They stay unseen unless mark_seen: true; headers_only: true fetches only the headers; raw: true adds the message as sent.

imap_set_flags(url: str, folder: str, uids: any, flags: any, opts: any) → intor an error

Sets flags ("seen", "flagged", "answered", "deleted", "draft", or keywords) on messages by UID; gives how many UIDs were named.

imap_clear_flags(url: str, folder: str, uids: any, flags: any, opts: any) → intor an error

Clears flags on messages by UID: "seen" makes them unread again.

imap_move(url: str, folder: str, uids: any, to: str, opts: any) → anyor an error

Moves messages by UID from folder to to: MOVE where the server has it, else COPY, mark deleted and UID EXPUNGE (only those messages). Gives { moved, method, new_uids } (new_uids where the server says, by UIDPLUS).

imap_append(url: str, folder: str, message: str, opts: any) → anyor an error

Adds a message (RFC 5322 text: headers, a blank line, the body) to folder, as a sent copy or a draft. Options: flags (["seen"]), also the connection's. Gives { folder, uid } (uid where the server says, by UIDPLUS).

imap_wait(url: str, folder: str, after_uid: int, wait_ms: int, opts: any) → listor an error

Waits up to wait_ms for messages in folder with a UID above after_uid, and gives their UIDs (at once when there are some already). Uses IDLE where the server has it, else asks every second; on a connection of its own, so other calls are not held up.

imap_close(url: str) → bool

Logs out and closes the sessions kept for url; whether there was one.

Get it

$ quillo install imap               # imap.qwer into this folder$ quillo install imap --example     # and its example, imap.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/imap.qwer$ curl -O https://quillolang.org/plugins/files/imap.qwe

Then, in your program, all of it or only what you call:

main.qwe
use imap.qweruse imap_folders, imap_status from imap      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }webpki-roots = "1"mail-parser = "0.9"

Example

imap.qwe
// A support inbox handled over IMAP: the folders, what is unread, a customer's reply read without// marking it seen, an invoice's attachment listed, searches by sender and date, a message marked// read and unread again, one archived, our answer kept in Sent, and a wait (IDLE) for new mail.////   IMAP_URL=imaps://support%40example.com:password@imap.example.com quillo run imap.qwe//   then open http://localhost:8080/unread, /read, /wait//// Needs an IMAP server at IMAP_URL (imaps://, or imap:// upgraded with STARTTLS); the user name and// password are percent-encoded in the URL. IMAP_CA is a CA's PEM for a private certificate.use "imap.qwer"let URL = env("IMAP_URL", "imaps://support%40example.com:password@127.0.0.1")let OPTS = { ca: env("IMAP_CA", ""), timeout_ms: 5000 }route GET /folders = map(imap_folders(URL, OPTS)?, (f) => { name: f.name, special: f.special })route GET /unread {    let uids = imap_search(URL, "INBOX", { unseen: true }, OPTS)?    let heads = imap_fetch(URL, "INBOX", uids, { headers_only: true, ca: OPTS.ca })?    { count: len(uids), messages: map(heads, (m) => { uid: m.uid, from: m.from.email, subject: m.subject }) }}route GET /read {    let m = imap_fetch(URL, "INBOX", 101, OPTS)?[0]    { subject: m.subject, from: m.from.name, text: m.text, still_unread: !m.seen }}route GET /invoice {    let m = imap_fetch(URL, "INBOX", 102, OPTS)?[0]    { subject: m.subject, attachments: m.attachments, has_html: m.html != null }}route GET /search = {    from_grace: imap_search(URL, "INBOX", { from: "grace@navy.example" }, OPTS)?,    since_29th: imap_search(URL, "INBOX", { since: "2026-10-29" }, OPTS)?,    invoice_unread: imap_search(URL, "INBOX", { subject: "invoice", unseen: true }, OPTS)?,}route GET /seen {    imap_set_flags(URL, "INBOX", [101], ["seen", "flagged"], OPTS)?    let after = imap_search(URL, "INBOX", { unseen: true }, OPTS)?    imap_clear_flags(URL, "INBOX", [101], "seen", OPTS)?    { unread_after_marking: after, unread_again: imap_search(URL, "INBOX", { unseen: true }, OPTS)?, flagged: imap_search(URL, "INBOX", { flagged: true }, OPTS)? }}route GET /archive {    let moved = imap_move(URL, "INBOX", [103], "Archive", OPTS)?    { moved, archive: imap_status(URL, "Archive", OPTS)?, inbox: imap_status(URL, "INBOX", OPTS)? }}// Our answer, kept in Sent as a mail client would after sending it.route GET /reply {    let answer = join([        "From: Support <support@quillo.test>",        "To: Ada Lovelace <ada@example.com>",        "Subject: Re: Re: Your order #1042",        "In-Reply-To: <reply-1042@example.com>",        "",        "Friday it is. We will ring twice.",    ], "\r\n")    let kept = imap_append(URL, "Sent", answer, { flags: ["seen"], ca: OPTS.ca })?    { kept, sent: map(imap_fetch(URL, "Sent", kept.uid, OPTS)?, (m) => m.subject) }}// New mail: waits (IDLE) for anything after the newest message we know of.route GET /wait {    let newest = imap_status(URL, "INBOX", OPTS)?.uidnext - 1    let fresh = imap_wait(URL, "INBOX", newest, 5000, OPTS)?    { new: fresh, subjects: len(fresh) > 0 ? map(imap_fetch(URL, "INBOX", fresh, OPTS)?, (m) => m.subject) : [] }}route GET /wrong = { error: imap_folders(replace(URL, "p%40ss", "guess"), OPTS).error }
$ quillo run imap.qwe$ curl 'localhost:8080/unread'$ curl 'localhost:8080/read'$ curl 'localhost:8080/seen'$ curl 'localhost:8080/archive'$ curl 'localhost:8080/reply'$ curl 'localhost:8080/wait'

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

imap.qwer
//! IMAP for inboxes a program reads and files: folders listed (special-use ones named), messages searched (unseen, since, from, subject...), fetched and read (subject, sender, date, text, HTML, attachments listed) without marking them seen, flags set and cleared, messages moved and appended, and a wait for new mail with IDLE; over IMAPS or STARTTLS, one session kept per account.//! 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, Instant};// ---- 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(),    })}// ---- IMAP responses ----#[derive(Debug, Clone)]enum Tok {    Atom(String),    Str(Vec<u8>),    List(Vec<Tok>),    Nil,}impl Tok {    fn text(&self) -> String {        match self {            Tok::Atom(s) => s.clone(),            Tok::Str(b) => String::from_utf8_lossy(b).to_string(),            Tok::List(_) => String::new(),            Tok::Nil => String::new(),        }    }    fn bytes(&self) -> Vec<u8> {        match self {            Tok::Str(b) => b.clone(),            Tok::Atom(s) => s.clone().into_bytes(),            _ => vec![],        }    }    fn list(&self) -> &[Tok] {        match self {            Tok::List(l) => l,            _ => &[],        }    }}/// One response line, with any literals ({n}) it carries read in.struct Resp {    parts: Vec<(Vec<u8>, Option<Vec<u8>>)>,}impl Resp {    fn first_text(&self) -> String {        String::from_utf8_lossy(&self.parts[0].0).to_string()    }    fn tokens(&self) -> Vec<Tok> {        let mut stack: Vec<Vec<Tok>> = vec![vec![]];        for (text, lit) in &self.parts {            let b = text;            let mut i = 0;            while i < b.len() {                let c = b[i];                match c {                    b' ' => i += 1,                    b'(' => {                        stack.push(vec![]);                        i += 1;                    }                    b')' => {                        if stack.len() > 1 {                            let l = stack.pop().unwrap();                            stack.last_mut().unwrap().push(Tok::List(l));                        }                        i += 1;                    }                    b'"' => {                        let mut s = vec![];                        i += 1;                        while i < b.len() && b[i] != b'"' {                            if b[i] == b'\\' && i + 1 < b.len() {                                i += 1;                            }                            s.push(b[i]);                            i += 1;                        }                        i += 1;                        stack.last_mut().unwrap().push(Tok::Str(s));                    }                    b'{' if lit.is_some() && b[i..].iter().all(|&x| x != b' ') => {                        // The literal's marker ends the text; its bytes come next.                        i = b.len();                    }                    _ => {                        let start = i;                        let mut depth = 0;                        while i < b.len() {                            let x = b[i];                            if x == b'[' {                                depth += 1;                            } else if x == b']' {                                depth -= 1;                            } else if depth == 0 && (x == b' ' || x == b'(' || x == b')') {                                break;                            }                            i += 1;                        }                        let a = String::from_utf8_lossy(&b[start..i]).to_string();                        stack.last_mut().unwrap().push(if a.eq_ignore_ascii_case("NIL") { Tok::Nil } else { Tok::Atom(a) });                    }                }            }            if let Some(l) = lit {                stack.last_mut().unwrap().push(Tok::Str(l.clone()));            }        }        while stack.len() > 1 {            let l = stack.pop().unwrap();            stack.last_mut().unwrap().push(Tok::List(l));        }        stack.pop().unwrap()    }}/// The literal a line ends with, `{123}` or `{123+}`.fn literal_len(line: &[u8]) -> Option<usize> {    if !line.ends_with(b"}") {        return None;    }    let open = line.iter().rposition(|&c| c == b'{')?;    std::str::from_utf8(&line[open + 1..line.len() - 1]).ok()?.trim_end_matches('+').parse().ok()}struct Imap {    c: Lines,    tag: u64,    caps: Vec<String>,    selected: Option<String>,    tls: bool,}/// A command's argument: an atom or quoted string as needed, or a literal for text that cannot be quoted.enum Arg {    Raw(String),    Text(String),    Bytes(Vec<u8>),}fn quote(s: &str) -> Option<String> {    if s.bytes().all(|b| b >= 0x20 && b < 0x7f) {        Some(format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")))    } else {        None    }}impl Imap {    fn read_resp(&mut self) -> Result<Resp, String> {        let mut parts = vec![];        loop {            let line = self.c.line()?;            match literal_len(&line) {                Some(n) => {                    let data = self.c.exact(n)?;                    parts.push((line, Some(data)));                }                None => {                    parts.push((line, None));                    return Ok(Resp { parts });                }            }        }    }    /// Sends a command and gives the untagged answers; NO or BAD is an error with the server's words.    fn run(&mut self, args: Vec<Arg>) -> Result<Vec<Resp>, String> {        self.tag += 1;        let tag = format!("Q{}", self.tag);        let mut line = tag.clone().into_bytes();        let shown: String = args.iter().take(1).map(|a| match a {            Arg::Raw(s) => {                let w: Vec<&str> = s.split(' ').collect();                if w[0] == "UID" && w.len() > 1 { format!("UID {}", w[1]) } else { w[0].to_string() }            }            _ => String::new(),        }).collect();        for a in args {            line.push(b' ');            match a {                Arg::Raw(s) => line.extend(s.into_bytes()),                Arg::Text(s) => match quote(&s) {                    Some(q) => line.extend(q.into_bytes()),                    None => {                        line.extend(format!("{{{}}}", s.len()).into_bytes());                        self.literal(&mut line, s.into_bytes())?;                    }                },                Arg::Bytes(b) => {                    line.extend(format!("{{{}}}", b.len()).into_bytes());                    self.literal(&mut line, b)?;                }            }        }        line.extend(b"\r\n");        self.c.send(&line)?;        let mut out = vec![];        loop {            let r = self.read_resp()?;            let first = r.first_text();            if first.starts_with(&format!("{} ", tag)) {                let rest = first[tag.len() + 1..].to_string();                if rest.to_ascii_uppercase().starts_with("OK") {                    out.push(r);                    return Ok(out);                }                return Err(format!("the server refused {}: {}", shown, rest));            }            if first.starts_with('+') {                continue;            }            out.push(r);        }    }    /// Sends what came before a literal, waits for the server's go-ahead ("+"), then the literal's bytes.    fn literal(&mut self, line: &mut Vec<u8>, data: Vec<u8>) -> Result<(), String> {        let plus = self.caps.iter().any(|c| c == "LITERAL+" || c == "LITERAL-");        if plus && data.len() <= 4096 {            line.pop();            line.extend(b"+}");            line.extend(b"\r\n");            line.extend(data);            return Ok(());        }        line.extend(b"\r\n");        self.c.send(line)?;        line.clear();        let r = self.read_resp()?;        if !r.first_text().starts_with('+') {            return Err(format!("the server would not take a literal: {}", r.first_text()));        }        line.extend(data);        Ok(())    }    fn select(&mut self, folder: &str) -> Result<Value, String> {        let rs = self.run(vec![Arg::Raw("SELECT".into()), Arg::Text(mutf7_encode(folder))])?;        self.selected = Some(folder.to_string());        let mut exists = 0;        let mut uidvalidity = Value::Null;        let mut uidnext = Value::Null;        for r in &rs {            let t = r.tokens();            if t.len() >= 3 && t[2].text().eq_ignore_ascii_case("EXISTS") {                exists = t[1].text().parse().unwrap_or(0);            }            let line = r.first_text().to_ascii_uppercase();            for (k, slot) in [("UIDVALIDITY ", &mut uidvalidity), ("UIDNEXT ", &mut uidnext)] {                if let Some(i) = line.find(&format!("[{}", k)) {                    let n: String = line[i + k.len() + 1..].chars().take_while(|c| c.is_ascii_digit()).collect();                    *slot = json!(n.parse::<i64>().unwrap_or(0));                }            }        }        Ok(json!({ "folder": folder, "exists": exists, "uidvalidity": uidvalidity, "uidnext": uidnext }))    }    fn ensure(&mut self, folder: &str) -> Result<(), String> {        if self.selected.as_deref() != Some(folder) {            self.select(folder)?;        }        Ok(())    }}// ---- Modified UTF-7 folder names (RFC 3501 5.1.3) ----fn mutf7_decode(s: &str) -> String {    let mut out = String::new();    let mut rest = s;    while let Some(i) = rest.find('&') {        out.push_str(&rest[..i]);        let after = &rest[i + 1..];        let end = after.find('-').unwrap_or(after.len());        if end == 0 {            out.push('&');        } else {            let b = b64_decode(&after[..end].replace(',', "/"));            let u: Vec<u16> = b.chunks(2).filter(|c| c.len() == 2).map(|c| u16::from_be_bytes([c[0], c[1]])).collect();            out.push_str(&String::from_utf16_lossy(&u));        }        rest = &after[(end + 1).min(after.len())..];    }    out.push_str(rest);    out}fn mutf7_encode(s: &str) -> String {    const T: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";    let mut out = String::new();    let mut run: Vec<u16> = vec![];    let flush = |run: &mut Vec<u16>, out: &mut String| {        if run.is_empty() {            return;        }        let bytes: Vec<u8> = run.iter().flat_map(|u| u.to_be_bytes()).collect();        out.push('&');        for chunk in bytes.chunks(3) {            let n = chunk.iter().enumerate().fold(0u32, |a, (i, b)| a | (*b as u32) << (16 - 8 * i));            for i in 0..=chunk.len() {                out.push(T[(n >> (18 - 6 * i) & 63) as usize] as char);            }        }        out.push('-');        run.clear();    };    for c in s.chars() {        if (' '..='~').contains(&c) {            flush(&mut run, &mut out);            if c == '&' { out.push_str("&-") } else { out.push(c) }        } else {            let mut b = [0u16; 2];            run.extend_from_slice(c.encode_utf16(&mut b));        }    }    flush(&mut run, &mut out);    out}// ---- Sessions ----static CONNS: Mutex<Option<HashMap<String, Imap>>> = Mutex::new(None);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 caps_of(line: &str) -> Vec<String> {    let up = line.to_ascii_uppercase();    let start = up.find("CAPABILITY").map(|i| i + 10);    match start {        Some(i) => up[i..].trim_end_matches(']').split(|c: char| c == ' ' || c == ']').filter(|s| !s.is_empty()).map(|s| s.to_string()).collect(),        None => vec![],    }}fn login(url: &str, opts: &Value) -> Result<Imap, String> {    let a = account(url, opts, "imap", "imaps", 143, 993)?;    let t = timeout_of(opts);    let tcp = connect(&a.host, a.port, t)?;    let mut s = Imap { c: Lines::new(if a.tls { tls_wrap(tcp, &a.host, opts)? } else { Stream::Plain(tcp) }), tag: 0, caps: vec![], selected: None, tls: a.tls };    let greet = s.read_resp()?.first_text();    if !greet.starts_with("* OK") && !greet.starts_with("* PREAUTH") {        return Err(format!("{} did not greet as IMAP does: {}", a.host, greet));    }    s.caps = caps_of(&greet);    let refresh = |s: &mut Imap| -> Result<(), String> {        let rs = s.run(vec![Arg::Raw("CAPABILITY".into())])?;        s.caps = rs.iter().flat_map(|r| caps_of(&r.first_text())).collect();        Ok(())    };    if s.caps.is_empty() {        refresh(&mut s)?;    }    let starttls = o_str(opts, "starttls").unwrap_or_else(|| "if_offered".into());    if !s.tls && starttls != "no" && starttls != "false" {        if s.caps.iter().any(|c| c == "STARTTLS") {            s.run(vec![Arg::Raw("STARTTLS".into())])?;            let Imap { c, .. } = s;            let Lines { s: st, .. } = c;            let Stream::Plain(tcp) = st else { unreachable!() };            s = Imap { c: Lines::new(tls_wrap(tcp, &a.host, opts)?), tag: 100, caps: vec![], selected: None, tls: true };            refresh(&mut s)?;        } else if starttls == "required" || starttls == "true" || starttls == "yes" {            return Err(format!("{} does not offer STARTTLS, and starttls is required", a.host));        }    }    if !s.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 imaps://, or allow_plain_password: true", a.host));    }    if greet.starts_with("* OK") {        if s.caps.iter().any(|c| c == "LOGINDISABLED") && !s.caps.iter().any(|c| c == "AUTH=PLAIN") {            return Err(format!("{} does not take LOGIN here (LOGINDISABLED): use TLS", a.host));        }        let rs = s.run(vec![Arg::Raw("LOGIN".into()), Arg::Text(a.user.clone()), Arg::Text(a.password.clone())]).map_err(|e| format!("sign-in failed: {}", e.trim_start_matches("the server refused LOGIN: ")))?;        let after: Vec<String> = rs.iter().flat_map(|r| caps_of(&r.first_text())).collect();        if !after.is_empty() {            s.caps = after;        } else {            refresh(&mut s)?;        }    }    Ok(s)}fn with<T>(key: &str, url: &str, opts: &Value, f: impl Fn(&mut Imap) -> Result<T, String>) -> Result<T, String> {    let t = turn(key);    let _turn = t.lock().unwrap_or_else(|e| e.into_inner());    let kept = CONNS.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(HashMap::new).remove(key);    let mut s = match kept {        Some(mut s) => {            let _ = s.c.s.tcp().set_read_timeout(Some(timeout_of(opts)));            // A kept session the server timed out is opened again.            if s.run(vec![Arg::Raw("NOOP".into())]).is_ok() { s } else { login(url, opts)? }        }        None => login(url, opts)?,    };    let r = f(&mut s);    let broken = matches!(&r, Err(e) if !e.starts_with("the server refused") && !e.starts_with("no ") && !e.starts_with("give "));    if !broken {        CONNS.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(HashMap::new).insert(key.to_string(), s);    }    r}fn uid_set(uids: &Value) -> Result<String, String> {    let list: Vec<i64> = match uids {        Value::Number(n) => vec![n.as_i64().unwrap_or(0)],        Value::Array(a) => a.iter().filter_map(|x| x.as_i64().or_else(|| x.as_str().and_then(|s| s.parse().ok()))).collect(),        Value::String(s) if s.chars().all(|c| c.is_ascii_digit() || ":,*".contains(c)) && !s.is_empty() => return Ok(s.clone()),        _ => return Err("give a UID, a list of UIDs, or a set such as \"100:*\"".into()),    };    if list.is_empty() || list.iter().any(|u| *u < 1) {        return Err("give UIDs from imap_search: whole numbers from 1".into());    }    Ok(list.iter().map(|u| u.to_string()).collect::<Vec<_>>().join(","))}fn flag_name(f: &str) -> String {    match f.trim_start_matches('\\').to_ascii_lowercase().as_str() {        "seen" | "read" => "\\Seen".into(),        "answered" | "replied" => "\\Answered".into(),        "flagged" | "starred" => "\\Flagged".into(),        "deleted" => "\\Deleted".into(),        "draft" => "\\Draft".into(),        _ => f.to_string(),    }}fn flags_of(v: &Value) -> Vec<String> {    match v {        Value::String(s) => vec![flag_name(s)],        Value::Array(a) => a.iter().filter_map(|x| x.as_str()).map(flag_name).collect(),        _ => vec![],    }}const MONTHS: [&str; 12] = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];/// 2026-10-01 as IMAP writes dates: 1-Oct-2026.fn imap_date(s: &str) -> Result<String, String> {    let p: Vec<&str> = s.get(..10).unwrap_or(s).split('-').collect();    if p.len() == 3 {        if let (Ok(y), Ok(m), Ok(d)) = (p[0].parse::<i32>(), p[1].parse::<usize>(), p[2].parse::<u32>()) {            if (1..=12).contains(&m) && (1..=31).contains(&d) {                return Ok(format!("{}-{}-{}", d, MONTHS[m - 1], y));            }        }    }    Err(format!("{} is not a date: write 2026-10-01", s))}/// Search criteria from a map, or as written when given as text.fn criteria(v: &Value, args: &mut Vec<Arg>, utf8: &mut bool) -> Result<(), String> {    match v {        Value::String(s) => {            args.push(Arg::Raw(s.clone()));            Ok(())        }        Value::Null => {            args.push(Arg::Raw("ALL".into()));            Ok(())        }        Value::Object(m) => {            if m.is_empty() {                args.push(Arg::Raw("ALL".into()));            }            for (k, val) in m {                let k = k.to_ascii_lowercase();                let on = val.as_bool();                let text = val.as_str().map(|s| s.to_string()).or_else(|| val.as_i64().map(|n| n.to_string()));                match k.as_str() {                    "all" => args.push(Arg::Raw("ALL".into())),                    "unseen" | "seen" | "flagged" | "unflagged" | "answered" | "unanswered" | "deleted" | "undeleted" | "draft" | "new" | "recent" => {                        let word = if on == Some(false) {                            match k.as_str() {                                "unseen" => "SEEN".to_string(),                                "seen" => "UNSEEN".to_string(),                                x if x.starts_with("un") => x[2..].to_ascii_uppercase(),                                x => format!("UN{}", x.to_ascii_uppercase()),                            }                        } else {                            k.to_ascii_uppercase()                        };                        args.push(Arg::Raw(word));                    }                    "since" | "before" | "on" | "sent_since" | "sent_before" => {                        let d = imap_date(&text.ok_or(format!("{}: give a date, 2026-10-01", k))?)?;                        args.push(Arg::Raw(format!("{} {}", k.replace('_', "").to_ascii_uppercase().replace("SENTSINCE", "SENTSINCE"), d)));                    }                    "from" | "to" | "cc" | "bcc" | "subject" | "body" | "text" => {                        let s = text.ok_or(format!("{}: give text", k))?;                        if !s.is_ascii() {                            *utf8 = true;                        }                        args.push(Arg::Raw(k.to_ascii_uppercase()));                        args.push(Arg::Text(s));                    }                    "larger" | "smaller" => args.push(Arg::Raw(format!("{} {}", k.to_ascii_uppercase(), val.as_i64().ok_or(format!("{}: give a size in bytes", k))?))),                    "uid" => args.push(Arg::Raw(format!("UID {}", uid_set(val)?))),                    "header" => {                        let pair = val.as_array().filter(|a| a.len() == 2).ok_or("header: give [name, text]")?;                        args.push(Arg::Raw(format!("HEADER {}", pair[0].as_str().unwrap_or(""))));                        args.push(Arg::Text(pair[1].as_str().unwrap_or("").to_string()));                    }                    "keyword" => args.push(Arg::Raw(format!("KEYWORD {}", text.unwrap_or_default()))),                    _ => return Err(format!("{} is not a search key: unseen, seen, flagged, answered, since, before, on, from, to, cc, subject, body, text, larger, smaller, uid, header, keyword", k)),                }            }            Ok(())        }        _ => Err("criteria are a map ({ unseen: true, from: \"ada\" }) or IMAP search text".into()),    }}fn fetch_items(rs: &[Resp]) -> Vec<HashMap<String, Tok>> {    let mut out = vec![];    for r in rs {        let t = r.tokens();        if t.len() >= 4 && t[0].text() == "*" && t[2].text().eq_ignore_ascii_case("FETCH") {            let l = t[3].list();            let mut m = HashMap::new();            let mut i = 0;            while i + 1 < l.len() {                let k = l[i].text().to_ascii_uppercase().replace(".PEEK", "");                m.insert(k, l[i + 1].clone());                i += 2;            }            out.push(m);        }    }    out}fn new_uids(s: &mut Imap, after: i64) -> Result<Vec<i64>, String> {    let rs = s.run(vec![Arg::Raw(format!("UID SEARCH UID {}:*", after + 1))])?;    let mut out = vec![];    for r in rs {        let t = r.tokens();        if t.len() >= 2 && t[1].text().eq_ignore_ascii_case("SEARCH") {            out.extend(t[2..].iter().filter_map(|x| x.text().parse::<i64>().ok()).filter(|u| *u > after));        }    }    out.sort();    Ok(out)}/// The folders of the account at `url` (imaps://user:password@host, or imap:// upgraded with STARTTLS): `[{ name, delimiter, flags, special }]`, special being "sent", "trash", "drafts", "junk", "archive" or "all" where the server says (RFC 6154). Options: `user`, `password`, `starttls` ("if_offered", "required", "no"), `ca`, `insecure`, `timeout_ms`, `allow_plain_password`. The session stays open for the next call.pub fn imap_folders(url: &str, opts: serde_json::Value) -> Result<Vec<Value>, String> {    with(url, url, &opts, |s| {        let rs = s.run(vec![Arg::Raw("LIST \"\" \"*\"".into())])?;        let mut out = vec![];        for r in rs {            let t = r.tokens();            if t.len() >= 5 && t[1].text().eq_ignore_ascii_case("LIST") {                let flags: Vec<String> = t[2].list().iter().map(|f| f.text()).collect();                let special = flags.iter().find_map(|f| match f.to_ascii_lowercase().as_str() {                    "\\sent" => Some("sent"),                    "\\trash" => Some("trash"),                    "\\drafts" => Some("drafts"),                    "\\junk" => Some("junk"),                    "\\archive" => Some("archive"),                    "\\all" => Some("all"),                    "\\flagged" => Some("flagged"),                    _ => None,                });                out.push(json!({ "name": mutf7_decode(&t[4].text()), "delimiter": t[3].text(), "flags": flags, "special": special }));            }        }        Ok(out)    })}/// How many messages `folder` holds, how many are unseen, and its UIDNEXT and UIDVALIDITY (STATUS).pub fn imap_status(url: &str, folder: &str, opts: serde_json::Value) -> Result<Value, String> {    with(url, url, &opts, |s| {        let rs = s.run(vec![Arg::Raw("STATUS".into()), Arg::Text(mutf7_encode(folder)), Arg::Raw("(MESSAGES UNSEEN UIDNEXT UIDVALIDITY)".into())])?;        for r in rs {            let t = r.tokens();            if t.len() >= 4 && t[1].text().eq_ignore_ascii_case("STATUS") {                let l = t[3].list();                let mut m = serde_json::Map::new();                m.insert("folder".into(), json!(folder));                let mut i = 0;                while i + 1 < l.len() {                    m.insert(l[i].text().to_ascii_lowercase().replace("messages", "count"), json!(l[i + 1].text().parse::<i64>().unwrap_or(0)));                    i += 2;                }                return Ok(Value::Object(m));            }        }        Err(format!("no STATUS answer for {}", folder))    })}/// The UIDs in `folder` that match `criteria`: a map such as `{ unseen: true, since: "2026-10-01", from: "ada@example.com", subject: "order" }` (also seen, flagged, answered, before, on, to, cc, body, text, larger, smaller, uid, header: [name, text], keyword), or IMAP search text.pub fn imap_search(url: &str, folder: &str, criteria_v: serde_json::Value, opts: serde_json::Value) -> Result<Vec<i64>, String> {    with(url, url, &opts, |s| {        s.ensure(folder)?;        let mut args = vec![];        let mut utf8 = false;        criteria(&criteria_v, &mut args, &mut utf8)?;        let mut cmd = vec![Arg::Raw(if utf8 { "UID SEARCH CHARSET UTF-8".into() } else { "UID SEARCH".into() })];        cmd.extend(args);        let rs = s.run(cmd)?;        let mut out = vec![];        for r in rs {            let t = r.tokens();            if t.len() >= 2 && t[1].text().eq_ignore_ascii_case("SEARCH") {                out.extend(t[2..].iter().filter_map(|x| x.text().parse::<i64>().ok()));            }        }        out.sort();        Ok(out)    })}/// Messages from `folder` by UID (one, a list, or a set such as "100:*"), read: `[{ uid, flags, seen, size, internal_date, subject, from, to, cc, date, message_id, in_reply_to, text, html, attachments: [{ name, type, size }] }]`. They stay unseen unless `mark_seen: true`; `headers_only: true` fetches only the headers; `raw: true` adds the message as sent.pub fn imap_fetch(url: &str, folder: &str, uids: serde_json::Value, opts: serde_json::Value) -> Result<Vec<Value>, String> {    let set = uid_set(&uids)?;    with(url, url, &opts, |s| {        s.ensure(folder)?;        let part = if o_bool(&opts, "headers_only", false) { "HEADER" } else { "" };        let body = if o_bool(&opts, "mark_seen", false) { format!("BODY[{}]", part) } else { format!("BODY.PEEK[{}]", part) };        let rs = s.run(vec![Arg::Raw(format!("UID FETCH {} (UID FLAGS RFC822.SIZE INTERNALDATE {})", set, body))])?;        let mut out = vec![];        for m in fetch_items(&rs) {            let raw = m.get(&format!("BODY[{}]", part)).map(|t| t.bytes()).unwrap_or_default();            let mut v = summary(&raw);            let flags: Vec<String> = m.get("FLAGS").map(|f| f.list().iter().map(|x| x.text()).collect()).unwrap_or_default();            v["uid"] = json!(m.get("UID").and_then(|u| u.text().parse::<i64>().ok()));            v["seen"] = json!(flags.iter().any(|f| f.eq_ignore_ascii_case("\\Seen")));            v["flags"] = json!(flags);            v["size"] = json!(m.get("RFC822.SIZE").and_then(|u| u.text().parse::<i64>().ok()));            v["internal_date"] = json!(m.get("INTERNALDATE").map(|d| d.text()));            if o_bool(&opts, "raw", false) {                v["raw"] = json!(String::from_utf8_lossy(&raw));            }            out.push(v);        }        out.sort_by_key(|v| v["uid"].as_i64().unwrap_or(0));        Ok(out)    })}fn store(url: &str, folder: &str, uids: &Value, flags: &Value, sign: &str, opts: &Value) -> Result<i64, String> {    let set = uid_set(uids)?;    let f = flags_of(flags);    if f.is_empty() {        return Err("give a flag or a list: \"seen\", \"flagged\", \"answered\", \"deleted\", \"draft\", or a keyword".into());    }    with(url, url, opts, |s| {        s.ensure(folder)?;        s.run(vec![Arg::Raw(format!("UID STORE {} {}FLAGS.SILENT ({})", set, sign, f.join(" ")))])?;        Ok(set.split(',').count() as i64)    })}/// Sets flags ("seen", "flagged", "answered", "deleted", "draft", or keywords) on messages by UID; gives how many UIDs were named.pub fn imap_set_flags(url: &str, folder: &str, uids: serde_json::Value, flags: serde_json::Value, opts: serde_json::Value) -> Result<i64, String> {    store(url, folder, &uids, &flags, "+", &opts)}/// Clears flags on messages by UID: "seen" makes them unread again.pub fn imap_clear_flags(url: &str, folder: &str, uids: serde_json::Value, flags: serde_json::Value, opts: serde_json::Value) -> Result<i64, String> {    store(url, folder, &uids, &flags, "-", &opts)}/// Moves messages by UID from `folder` to `to`: MOVE where the server has it, else COPY, mark deleted and UID EXPUNGE (only those messages). Gives `{ moved, method, new_uids }` (new_uids where the server says, by UIDPLUS).pub fn imap_move(url: &str, folder: &str, uids: serde_json::Value, to: &str, opts: serde_json::Value) -> Result<Value, String> {    let set = uid_set(&uids)?;    with(url, url, &opts, |s| {        s.ensure(folder)?;        let new_uids = |rs: &[Resp]| -> Value {            for r in rs {                let up = r.first_text().to_ascii_uppercase();                if let Some(i) = up.find("[COPYUID ") {                    let parts: Vec<&str> = up[i + 9..].trim_end_matches(|c| c != ']').trim_end_matches(']').split(' ').collect();                    if parts.len() >= 3 {                        return json!(parts[2].split(']').next().unwrap_or(""));                    }                }            }            Value::Null        };        if s.caps.iter().any(|c| c == "MOVE") {            let rs = s.run(vec![Arg::Raw(format!("UID MOVE {}", set)), Arg::Text(mutf7_encode(to))])?;            return Ok(json!({ "moved": set.split(',').count(), "method": "MOVE", "new_uids": new_uids(&rs) }));        }        let rs = s.run(vec![Arg::Raw(format!("UID COPY {}", set)), Arg::Text(mutf7_encode(to))])?;        s.run(vec![Arg::Raw(format!("UID STORE {} +FLAGS.SILENT (\\Deleted)", set))])?;        if s.caps.iter().any(|c| c == "UIDPLUS") {            s.run(vec![Arg::Raw(format!("UID EXPUNGE {}", set))])?;            Ok(json!({ "moved": set.split(',').count(), "method": "COPY", "new_uids": new_uids(&rs) }))        } else {            Ok(json!({ "moved": set.split(',').count(), "method": "COPY, marked deleted (the server has no UIDPLUS: expunge it yourself)", "new_uids": new_uids(&rs) }))        }    })}/// Adds a message (RFC 5322 text: headers, a blank line, the body) to `folder`, as a sent copy or a draft. Options: `flags` (["seen"]), also the connection's. Gives `{ folder, uid }` (uid where the server says, by UIDPLUS).pub fn imap_append(url: &str, folder: &str, message: &str, opts: serde_json::Value) -> Result<Value, String> {    let body = message.replace("\r\n", "\n").replace('\n', "\r\n");    if !body.contains("\r\n\r\n") {        return Err("a message is headers, a blank line, then the body".into());    }    let flags = flags_of(opts.get("flags").unwrap_or(&Value::Null));    with(url, url, &opts, |s| {        let rs = s.run(vec![Arg::Raw("APPEND".into()), Arg::Text(mutf7_encode(folder)), Arg::Raw(format!("({})", flags.join(" "))), Arg::Bytes(body.clone().into_bytes())])?;        let mut uid = Value::Null;        for r in &rs {            let up = r.first_text().to_ascii_uppercase();            if let Some(i) = up.find("[APPENDUID ") {                let p: Vec<&str> = up[i + 11..].split(|c| c == ' ' || c == ']').collect();                if p.len() >= 2 {                    uid = json!(p[1].parse::<i64>().ok());                }            }        }        if s.selected.as_deref() == Some(folder) {            s.selected = None;        }        Ok(json!({ "folder": folder, "uid": uid }))    })}/// Waits up to `wait_ms` for messages in `folder` with a UID above `after_uid`, and gives their UIDs (at once when there are some already). Uses IDLE where the server has it, else asks every second; on a connection of its own, so other calls are not held up.pub fn imap_wait(url: &str, folder: &str, after_uid: i64, wait_ms: i64, opts: serde_json::Value) -> Result<Vec<i64>, String> {    let key = format!("{}#wait", url);    let until = Instant::now() + Duration::from_millis(wait_ms.clamp(0, 29 * 60_000) as u64);    with(&key, url, &opts, |s| {        s.select(folder)?;        loop {            let got = new_uids(s, after_uid)?;            if !got.is_empty() || Instant::now() >= until {                return Ok(got);            }            if s.caps.iter().any(|c| c == "IDLE") {                s.tag += 1;                let tag = format!("Q{}", s.tag);                s.c.send(format!("{} IDLE\r\n", tag).as_bytes())?;                let r = s.read_resp()?;                if !r.first_text().starts_with('+') {                    return Err(format!("IDLE refused: {}", r.first_text()));                }                let _ = s.c.s.tcp().set_read_timeout(Some(Duration::from_millis(200)));                let mut news = false;                while !news && Instant::now() < until {                    match s.c.line() {                        Ok(l) => {                            let t = String::from_utf8_lossy(&l).to_ascii_uppercase();                            if t.starts_with("* ") && (t.ends_with(" EXISTS") || t.ends_with(" RECENT")) {                                news = true;                            }                        }                        Err(e) if e.contains("in time") => {}                        Err(e) => return Err(e),                    }                }                let _ = s.c.s.tcp().set_read_timeout(Some(timeout_of(&opts)));                s.c.send(b"DONE\r\n")?;                loop {                    let l = s.c.line()?;                    if l.starts_with(tag.as_bytes()) {                        break;                    }                }            } else {                std::thread::sleep(Duration::from_millis(1000).min(until.saturating_duration_since(Instant::now())));                s.run(vec![Arg::Raw("NOOP".into())])?;            }        }    })}/// Logs out and closes the sessions kept for `url`; whether there was one.pub fn imap_close(url: &str) -> bool {    let mut any = false;    for key in [url.to_string(), format!("{}#wait", url)] {        let t = turn(&key);        let _turn = t.lock().unwrap_or_else(|e| e.into_inner());        let kept = CONNS.lock().unwrap_or_else(|e| e.into_inner()).get_or_insert_with(HashMap::new).remove(&key);        if let Some(mut s) = kept {            let _ = s.run(vec![Arg::Raw("LOGOUT".into())]);            any = true;        }    }    any}
POP3 pop3.qwer · 1.0.0Old 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.emailsupportnetworktls7 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.emailimportsupportattachments8 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.emailimportexportsupportattachments7 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.emailimportattachmentssupport6 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.emailnetworkdnsabuse4 views · 8 downloadsDNS dns.qwer · 1.0.0DNS from the server: A, AAAA, MX, TXT, CNAME, NS, SOA, SRV, CAA and PTR records looked up through the system resolver or one you name, host names resolved, and record text and zone files read into fields.dnsnetworkemail7 views · 8 downloads
Thread
77Useful? Like it, or add to its thread below.

Thread 0 posts

On IMAP: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.