--- /dev/null
+(
+ (c-mode
+ . (
+ (c-file-style . "awk")
+ (indent-tabs-mode . t)
+ ))
+
+ (rust-mode
+ . (
+ (eglot-server-programs (rust-mode . (eglot-rls "@abs_builddir@/rls")))
+ (indent-tabs-mode . nil)
+ ))
+ )
--- /dev/null
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "bitflags"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
+
+[[package]]
+name = "byteorder"
+version = "1.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de"
+
+[[package]]
+name = "cc"
+version = "1.0.60"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef611cc68ff783f18535d77ddd080185275713d852c4f5cbb6122c462a7a825c"
+
+[[package]]
+name = "cfg-if"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
+
+[[package]]
+name = "imap-folder-manager"
+version = "0.0.0"
+dependencies = [
+ "libc",
+ "log",
+ "managed-folders",
+ "nmh",
+ "nmhimapstore",
+ "nmhimaptypes",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.78"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa7087f49d294270db4e1928fc110c976cd4b9e5a16348e0a1df09afa99e6c98"
+
+[[package]]
+name = "lmdb-rkv"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "447a296f7aca299cfbb50f4e4f3d49451549af655fb7215d7f8c0c3d64bad42b"
+dependencies = [
+ "bitflags",
+ "byteorder",
+ "libc",
+ "lmdb-rkv-sys",
+]
+
+[[package]]
+name = "lmdb-rkv-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b27470ac25167b3afdfb6af8fcd3bc1be67de50ffbdaf4073378cfded6ae24a5"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "log"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcf3805d4480bb5b86070dcfeb9e2cb2ebc148adb753c5cca5f884d1d65a42b2"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "managed-folders"
+version = "0.0.0"
+dependencies = [
+ "log",
+ "nmh",
+]
+
+[[package]]
+name = "mhimap"
+version = "0.0.0"
+dependencies = [
+ "nmh",
+ "nmhimapstore",
+]
+
+[[package]]
+name = "nmh"
+version = "0.0.0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "nmhimapstore"
+version = "0.0.0"
+dependencies = [
+ "lmdb-rkv",
+ "lmdb-rkv-sys",
+ "log",
+ "nmh",
+ "nmhimaptypes",
+]
+
+[[package]]
+name = "nmhimaptypes"
+version = "0.0.0"
+dependencies = [
+ "nmh",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33"
--- /dev/null
+[workspace]
+members = ["imap/folder-manager", "imap/mhimap"]
##
TESTS_ENVIRONMENT = MH_OBJ_DIR="@abs_builddir@" \
MH_VERSION="$(VERSION)" \
+ MAKE=$(MAKE) \
OAUTH_SUPPORT='@OAUTH_SUPPORT@' \
CURL_USER_AGENT='@CURL_USER_AGENT@' \
MH_TEST_DIR=`cd "@abs_builddir@" && pwd -P`/test/testdir \
test/burst/test-burst \
test/burst/test-burst-mime \
test/comp/test-comp-format \
+ test/comp/test-draft-fcc \
test/dist/test-dist \
test/folder/test-coverage \
test/folder/test-create \
test/forw/test-forw-coverage \
test/forw/test-forw-digest \
test/forw/test-forw-format \
+ test/imap/test-inc \
+ test/imap/test-live \
+ test/imap/test-scan \
test/inc/test-deb359167 \
test/inc/test-eom-align \
test/inc/test-inc-scanout \
test/install-mh/test-version-check \
test/locking/test-datalocking \
test/locking/test-spoollocking \
+ test/managed-folders/test-read-message \
test/manpages/test-manpages \
test/mhbuild/test-attach \
test/mhbuild/test-cte \
test/whom/test-whom \
test/cleanup ## The "cleanup" test should always be last.
+check_DATA = fake.pem
+
check_SCRIPTS = test/common.sh
check_PROGRAMS = \
test/fakehttp \
+ test/fakeinetd \
test/fakepop \
test/fakesmtp \
test/getcanon \
test/getcwidth \
test/getfullname \
+ test/managed-folders/protocol-tester \
test/runpty \
#
h/dropsbr.h \
h/fmt_compile.h \
h/fmt_scan.h \
+ h/folder.h \
+ h/folder_manager_protocol.h \
h/icalendar.h \
+ h/managed_folders.h \
h/mf.h \
h/mh.h \
h/mhparse.h \
test_fakehttp_SOURCES = test/fakehttp.c test/server.c
test_fakehttp_LDADD = $(LDADD) $(POSTLINK)
+test_fakeinetd_SOURCES = test/fakeinetd.c
+test_fakeinetd_LDADD = $(LDADD) $(POSTLINK)
+
test_getcwidth_SOURCES = test/getcwidth.c
test_getcwidth_LDADD = $(POSTLINK)
sbr/folder_addmsg.c \
sbr/folder_delmsgs.c \
sbr/folder_free.c \
+ sbr/folder_manager_client.c \
+ sbr/folder_manager_protocol.c \
+ sbr/folder_operations.c \
sbr/folder_pack.c \
sbr/folder_read.c \
sbr/folder_realloc.c \
sbr/m_rand.c \
sbr/maildir_read_and_sort.c \
sbr/makedir.c \
+ sbr/managed_folders.c \
sbr/message_id.c \
sbr/mf.c \
sbr/mime_type.c \
.PHONY: gcov gcov-rebuild gcov-run gcov-process gcov-report \
gcov-mostlyclean gcov-clean
+# TODO rust coverage
+# cargo +nightly install cargo-binutils grcov rustfilt
+# RUSTFLAGS=-Zinstrument-coverage make CARGO_FLAGS=+nightly cargo
+# make check
+# TODO rename .llvmprof to .profdata so grcov will see it
+# cargo +nightly profdata -- merge -sparse test/imap/*.llvmprof -o profdata
+# cargo +nightly cov -- show target/debug/imap-folder-manager -instr-profile=profdata "-path-equivalence=,${top_srcdir}"
+# grcov --branch --llvm -b target/debug -o ~/public_html/cov -t html -s ../.. test/imap/cov
##
## Rules to upload the distribution to savannah
@echo "Uploading to savannah"
scp -p $(DIST_ARCHIVES)* $(SAVANNAH_USERNAME)@dl.sv.nongnu.org:/releases/nmh
.PHONY: upload
+
+# TODO autoconf check for cargo and bindgen
+.PHONY: cargo cargo-doc
+cargo: $(srcdir)/sbr/rust/src/bindgen.rs $(abs_srcdir)/sbr/rust/build.rs
+ cd $(srcdir) && CARGO_TARGET_DIR=$(abs_builddir)/target cargo $(CARGO_FLAGS) build
+cargo-doc: cargo
+ cd $(srcdir) && CARGO_TARGET_DIR=$(abs_builddir)/target cargo $(CARGO_FLAGS) doc --no-deps
+
+# TODO default to release build with optional debug build
+# TODO install mhimap to bindir and imap-folder-manager to libexecdir
+# TODO proper build rules, including dependency on libmh.a
+target/debug/mhimap target/debug/imap-folder-manager: cargo
+
+# TODO proper out-of-tree build
+$(abs_srcdir)/sbr/rust/build.rs: $(abs_builddir)/sbr/rust/build.rs
+ [ $(abs_srcdir) = $(abs_builddir) ] || cp $(builddir)/sbr/rust/build.rs $(srcdir)/sbr/rust/build.rs
+
+$(srcdir)/sbr/rust/src/bindgen.rs: $(srcdir)/sbr/rust/bindgen.h
+ bindgen --no-derive-copy -o $(srcdir)/sbr/rust/src/bindgen.rs.tmp $(srcdir)/sbr/rust/bindgen.h -- -I$(srcdir) \
+ && mv $(srcdir)/sbr/rust/src/bindgen.rs.tmp $(srcdir)/sbr/rust/src/bindgen.rs
+
+# TODO autoconf check for openssl
+fake.pem:
+ yes '' | openssl req -new -x509 -nodes -out fake.pem -keyout fake.pem
OAuth support : ${oauth_support}
])])dnl
+dnl TODO move up with other as-yet-unwritten rust configuration
+dnl TODO ac_pwd doesn't seem right
+CARGO_TARGET_DIR=$ac_pwd/target
+AC_SUBST([CARGO_TARGET_DIR])
+dnl TODO
+RLS=rls
+AC_SUBST([RLS])
+
dnl ---------------
dnl OUTPUT MAKEFILE
dnl ---------------
-AC_CONFIG_FILES([Makefile test/common.sh])
+AC_CONFIG_FILES([.dir-locals.el], [cp .dir-locals.el $ac_abs_srcdir])
+AC_CONFIG_FILES([
+ Makefile
+ sbr/rust/build.rs
+ test/common.sh
+])
+AC_CONFIG_FILES([rls], [chmod +x rls])
AC_CONFIG_FILES([uip/spost], [chmod +x uip/spost])
+
AC_OUTPUT
--- /dev/null
+#+TITLE: IMAP Synchronization for nmh
+
+* Status
+
+An EXPERIMENTAL, almost-entirely-happy-path-only partial implementation
+exists. Most of the nmh commands that are within the scope of the project
+have not yet been updated.
+
+All functionality behind --enable-experimental-imap-folder-manager
+configuration flag.
+
+* Rationale
+
+The world is now mobile-first, and many of are are mobile-only, or nearly so.
+When my smart phone took over my life, two things happened to my nmh usage:
+
+- At work, I was frustrated constantly by messages read under nmh being unread
+ on my phone and vice versa.
+- In my personal life, that same frustration drove me away from nmh completely.
+ Whenever I need to reply to email from my computer, I find my self
+ incorporating months of messages.
+
+We can use IMAP to make nmh a first-class citizen in the modern mail world.
+
+* Goals
+
+- Treat the IMAP server as the source of truth.
+- Minimize startup penalty to nmh commands.
+- Minimize the impact to the nmh code-base.
+- Minimize round-trips to the server.
+- Enhance =inc= to incorporate mail from a hierarchy of IMAP mailboxes.
+- Enhance =inc= to delete mail locally that has been deleted on the server.
+- Enhance all commands interpreting the user's =Unseen-Sequence= (i.e. all those
+ taking message specifications (=mh-sequence(5)=), e.g. =mark=, =pick=, =scan=,
+ =show=) to interpret references to the =Unseen-Sequence= as all messages not
+ flagged as =\Seen= on the server.
+- Enhance =scan= output to reflect live =\Seen= status when it is asked to
+ display whether messages appear in the user's =Unseen-Sequence=.
+- Enhance =show= to set the =\Seen= flag on messages shown.
+- Enhance =mark= to set or clear the =\Seen= flag for messages removed from or
+ added to the user's =Unseen-Sequence=, respectively.
+- Enhance =rmm= to delete messages from the server.
+- Enhance =refile= to move messages on the server.
+- Enhance =comp= and =repl= to allow usage of a server-side draft folder.
+
+** Deferred goals
+
+- Off-line mode might be useful
+- Synchronizing flags other than =\Seen=
+- Storing sequences on the server (possibly the same problem as the previous)
+- IMAP defines a message UID as an unsigned 32-bit integer, allowing for
+ 4,294,967,295 messages, but nmh uses SIGNED =int=, which is 32 bits on
+ amd64, allowing for only 2,147,483,647 messages. It's not clear any file
+ system offers reasonable performance with directories of that size, so we
+ may need to redesign nmh folders before the =int= becomes a problem.
+ Assuming any of this is a practical concern...
+
+* Approach
+
+** Architecture
+
+*** Managed folders
+
+"Minimize startup penalty" and "Minimize round-trips" suggest we start a
+separate process which can keep state between nmh commands. Each nmh command
+does not have to can pay the relatively expensive TLS connection setup cost.
+
+"Minimize impact to the nmh code-base" means continuing to represent folders
+ as directories and messages as numbered files within.
+
+To solve those, we introduce managed folders, which are folders in the nmh
+mail store managed by a folder manager. When the user requests an operation
+on a managed folder, we start by informing the folder manager of the operation
+to be taken. In some cases, the folder does nearly all the work. In other
+cases, it simply saves some information from the server where the rest of nmh
+can easily find it and then gets out of the way.
+
+For example, =scan= hands the user-specified message sequences to the folder
+manager, which consults the IMAP server and stores the results in a special
+sequence, and then the rest of =scan= as we all know and love it takes over,
+blissfully unaware the folder is special.
+
+*** Source of truth
+
+"Treat the IMAP server as the source of truth" gives us a simple and
+robust model.
+
+If a user =rmm= a message, we first delete it from the server. If that fails,
+we report the failure to the user and do not proceed. The message remains in
+both the nmh folder and on the IMAP server. If deletion from the server
+succeeds, but we fail to delete locally, we still report an error, but the
+message is now treated as though some other client had deleted it from the
+server: we will delete it on the next =inc=.
+
+** Language
+
+I've programmed in C and C++ for most of my career and I've just about had it
+with unsafe languages. The thought of implementing IMAP synchronization in C
+discouraged me from even starting for years.
+
+In my last position at Google, my fellow tech lead and I would often joke
+about dividing the team in two, with one team continuing to chase down a
+memory corruption error we'd failed for months to find, and the other
+rewriting the system in Rust. It was a joke, but the idea stuck with me.
+
+Since Rust supports the C ABI, we can seamlessly embed Rust into nmh programs.
+Why not give it a shot?
+
+In this case it's even easier: write the folder manager in Rust.
+
+* Implementation
+
+** Managed folders
+
+*** New profile parameters
+
+- =Managed-Folders= is a space-separated list of folders to be managed.
+ Each folder must have a corresponding =[folder]-manager= component naming the
+ folder manager.
+- =[folder]-manager= where =folder= is the name of the folder manager to start.
+- [folder]-socket= is the path of the UNIX domain socket.
+
+*** Starting the folder manager
+
+1. Try to connect to running manager through =[folder]-socket=. If that works, done!
+2. If error was not ~ENOENT~, ~unlink(2)~ the stale socket.
+3. ~pipe(2)~
+4. ~fork~
+5. child - folder-manager; prints to stdout if successfully started (e.g. connected to IMAP)
+5. parent - read from pipe to child
+6. parent - if error, die
+7. parent - else, time to connect again! this time, die if error
+
+*** Protocol
+
+The protocol isn't the best, and we might want to replace it at some point, but
+I have no plans to do that until finishing the first feature set.
+
+Before refining this protocol, we should look into IPC.
+
+nmh commands and the folder manager communicate over a UNIX domain socket
+configured in the MH profile. An nmh command opens a connection to the folder
+manager and sends it a message encoded as a standard mail message header.
+The folder manager replies in the same format.
+
+Unlike in mail messages, these messages MUST start with a =len= component whose
+value is the total length of the messgae including the =len= component itself.
+
+**** Common messages
+
+The following common message types are defined:
+
+***** Folder
+
+This message contains only a single folder name.
+
+#+BEGIN_EXAMPLE
+folder:...
+#+END_EXAMPLE
+
+***** Mark
+
+This message contains a folder in which to operate, specifications of which
+messages to pick and sequences to add to or remove from.
+
+#+BEGIN_EXAMPLE
+folder:...
+msgs:...
+seqs:...
+#+END_EXAMPLE
+
+***** Empty response
+
+Many operations have nothing else to respond with but status and optional error message.
+
+#+BEGIN_EXAMPLE
+status:OK | NOTOK
+error:error message present only if status:NOTOK
+#+END_EXAMPLE
+
+**** inc
+
+=inc= sends the request to the folder manager, which opens the local database to
+see which which messages exist, sends IMAP =FETCH= requests (up to
+=max-fetch-messages-in-flight= at a time), stores the messages, and sends one
+response back to =inc= per message, so =inc= can scan we go.
+
+***** request
+
+The =inc= request is =op:inc= followed by the folder message:
+
+#+BEGIN_EXAMPLE
+op:inc
+folder:...
+#+END_EXAMPLE
+
+***** response
+
+THe inc response is a series of messages.
+
+First, one inc message response per message:
+
+#+BEGIN_EXAMPLE
+status:INC_FOLDER
+folder:...
+msg:N
+unseen:N
+#+END_EXAMPLE
+
+Finally a status:OK empty response indicates completion.
+
+**** mark-add / mark-del
+
+These operations are inverse of each other, so request and response are the same.
+
+***** request
+
+#+BEGIN_EXAMPLE
+op:mark-(add / del)
+folder:...
+msgs:...
+seqs:...;
+#+END_EXAMPLE
+
+***** response
+
+The empty response.
+
+**** TODO the rest
+
+** IMAP
+
+*** New profile parameters
+
+- [folder]-host, [folder]-port - to connect
+- [folder]-tls TLS configuration
+- [folder]-user: username
+- [folder]-auth: PREAUTH | SASL - how to authenticate
+- [folder]-authservice: for XOAUTH2
+- [folder]-saslmech: force a SASL mechanism in case nmh chooses the wrong default
+- [folder]-db: path to the database
+- [folder]-mailbox-root: only look for mailboxes under this root
+
+*** Folder store
+
+imap-folder-manager needs to store the following per folder (mailbox):
+
+- forward and reverse map of message UID and local message number
+- mailbox UIDNEXT
+- mailbox UIDVALIDITY
+
+We store UIDNEXT and UIDVALIDITY in plain text files. For the UID maps, we
+need something fast.
+
+LMDB is a simple and fast embedded memory-mapped database library with ACID
+semantics, and seems like a reasonable choice. It might be worth trying out
+some alternatives, but this is a fine place to start.
+
+lmdb-rkv is a Rust wrapper from Mozilla which seemed like a decent choice in 2020,
+but that was before the Great Rust Layoff, and it now seems to be abandoned.
+It might be worth looking into taking it over, or finding an alternative, or just
+building our own simple binding only exposing what we need.
+
+*** mhimap - a new nmh command for manipulating the imap-folder-manager
+
+Currently, the only facilities this offers are for the tests. They are:
+
+- start
+- shutdown
+- noop
+- check
+
+=shutdown= may be useful to users who don't want to leave the authenticated
+process running indefinitely.
+
+=check= verifies the integrity of the synchronization database.
+
+*** Code organization
+
+**** /sbr/rust - crate nmh
+
+=nmh= is the base crate. It offers Rusty wrappers around the core subroutines
+in the =sbr= directory (=libmh.a=).
+
+**** /managedfolders
+
+The =managed_folders= crate implements the folder-manager protocol.
+Nothing specific to IMAP is here; if anyone wanted to implement a different
+folder manager, they could use this.
+
+**** /imap/types
+
+Exposes types common to the 4 IMAP crates.
+
+**** /imap/store
+
+This crate implements Folder store described above.
+
+**** /imap/folder-manager
+
+This is =imap-folder-manager= itself.
+
+**** /imap/mhimap
+
+This is the =mhimap= command described above.
+
+** Dependencies
+
+*** Direct
+libc = 0.2.78
+lmdb-rkv = 0.14
+lmdb-rkv-sys = 0.11
+log = 0.4
+
+*** Indirect
+bitflags = 1.2.1
+byteorder = 1.3
+cc = 1.0.60
+cfg-if = 0.1.10
+pkg-config = 0.3.18
+
+*** Unstable
+cfg-if = "0.1.10"
+libc = "0.2.78"
+lmdb-rkv = "0.14.0"
+lmdb-rkv-sys = "0.11.0"
+log = "0.4.13"
+pkg-config = "0.3.18"
+
+*** License
+pkg-config MIT/Apache-2.0
+bitflags MIT/Apache-2.0
+byteorder MIT
+cc MIT/Apache-2.0
+cfg-if MIT/Apache-2.0
+libc MIT OR Apache-2.0
+lmdb-rkv Apache-2.0
+lmdb-rkv-sys Apache-2.0
+log MIT OR Apache-2.0
+
+* Testing
+
+Look in the =testing= directory beside this file for more information.
+
+- =tests/imap/test-live= exercises as much of the IMAP functionality as possible
+ against any supported server. Because we run it against all servers, any test
+ that can live here should live here.
+
+- New tests using the common nmh test suite, run under =make check=.
+ These tests use the classic Panda IMAP server. =make check= also includes
+ =tests/imap/test-live=, against Panda only. Add test cases at this level only
+ if they can't be written for =test-live=.
+
+- Rust "integration tests", which go in the =tests= directory of each crate.
+ Add test cases at this level only if they can't be written for the common nmh
+ test suite.
+
+- Rust "unit tests", which go in =tests= sub-modules under the =src= directory
+ of each crate. Add test cases at this level only if they can't be written as
+ Rust "integration tests".
+
+* Supported servers somewhere
+
+** Free implementations
+
+- Cyrus
+- Dovecot
+- Panda
+
+** Service providers
+
+- Gandi (they seem to run Dovecot)
+- GMail
--- /dev/null
+START {
+ recover cmd="ctl_cyrusdb -r"
+}
+
+SERVICES {
+ imap cmd="imapd" listen="127.0.0.1:imap" prefork=0
+}
+
+EVENTS {
+ checkpoint cmd="ctl_cyrusdb -c" period=30
+ delprune cmd="cyr_expire -E 3" at=0400
+ deleteprune cmd="cyr_expire -E 4 -D 28" at=0430
+ expungeprune cmd="cyr_expire -E 4 -X 28" at=0445
+ tlsprune cmd="tls_prune" at=0400
+}
+
+DAEMON {
+}
--- /dev/null
+configdirectory: /var/imap
+partition-default: /var/spool/imap
+
+unixhierarchysep: yes
+
+allowplaintext: yes
+
+autocreate_quota: 0
+
+sasl_pwcheck_method: auxprop
+
+# TODO unneeded right?
+sieveusehomedir: false
+sievedir: /var/imap/sieve
--- /dev/null
+base_dir = /home/epg/work/nmh/.o/std/dovecot
+state_dir = /home/epg/work/nmh/.o/std/dovecot/db
+log_path = /home/epg/work/nmh/.o/std/dovecot/log
+
+default_internal_user = epg
+default_login_user = epg
+default_internal_group = epg
+
+mail_location = mbox:/home/epg/work/nmh/.o/std/dovecot/db/mail/%d/%1n/%n:INDEX=/home/epg/work/nmh/.o/std/dovecot/db/indexes/%d/%1n/%n
+
+protocols = imap
+listen = 127.0.0.1
+
+passdb {
+ driver = static
+ args = password=testpw
+}
+
+namespace inbox {
+ inbox = yes
+}
+
+service anvil {
+ chroot =
+}
+
+service imap-login {
+ chroot =
+ inet_listener imap {
+ port = 1043
+ }
+ inet_listener imaps {
+ port = 1044
+ }
+}
--- /dev/null
+* cyrus
+
+~sudo saslpasswd2 -c test~
+
+* panda
+
+All IMAP tests run against panda with a simple ~make check~.
--- /dev/null
+#!/usr/local/bin/zsh
+
+# Quick-n-dirty script to ease running the IMAP tests in various configurations.
+# Also generates coverage reports (requires Rust nightly).
+# TODO redo in more standard fashion under `make check`.
+
+# Configuration:
+# export MH_TEST_IMAP_LOG_LEVEL=trace
+# export MH_TEST_NOCLEANUP=1
+# export MH_TEST_STUNNEL_DEBUG=7
+# export MH_TEST_STUNNEL_OUTPUT=/dev/stderr
+# export MH_TEST_VERBOSE=1
+# export RUST_BACKTRACE=1
+# abs_srcdir=$HOME/work/nmh
+# abs_builddir=$abs_srcdir/.o/clang13
+# LLVM_PROFDATA=llvm-profdata13
+# LLVM_COV=llvm-cov13
+# MAKE=make
+# MAKE_JOBS=-j4
+# PANDA_IMAPD=/usr/local/libexec/imapd
+# # optional:
+# gandi_user='example@example.com'
+# gandi_pw='...'
+#gmail_user='example@example.com'
+#gmail_oauth=/path/to/oauth-gmail
+
+set -ex
+
+abs_builddir=$PWD
+
+cf=$1
+if ! [[ -n $cf ]]; then
+ print -u2 "usage imaptest CONFIGURATION ACTION"
+ exit 2
+fi
+shift
+
+. $cf
+
+for v in \
+ abs_srcdir \
+ LLVM_PROFDATA \
+ LLVM_COV \
+ MAKE \
+ PANDA_IMAPD \
+ ; do
+ if ! eval "[[ -n \$$v ]]"; then
+ print -u2 "$v must be set in $cf"
+ exit 2
+ fi
+done
+
+testdir=$abs_builddir/test/testdir
+
+export CARGO_TARGET_DIR=$abs_builddir/target
+
+# Coverage documentation
+# https://doc.rust-lang.org/beta/unstable-book/compiler-flags/source-based-code-coverage.html
+# https://clang.llvm.org/docs/SourceBasedCodeCoverage.html
+# https://llvm.org/docs/CommandGuide/llvm-profdata.html
+# https://llvm.org/docs/CommandGuide/llvm-cov.html
+# Bugs
+# https://github.com/rust-lang/rust/issues/79121 Tracking Issue for LLVM InstrProf code coverage
+# https://github.com/rust-lang/rust/issues/85902 Add a cargo option to generate coverage reports using -Z instrument-coverage
+# https://github.com/rust-lang/rust/issues/79649 Add support for branch coverage in source-based coverage
+report-coverage() {
+ typeset profile=$1
+ typeset profile_dir=$2
+ typeset profdata=$abs_builddir/covdata/$profile.profdata
+ find $abs_builddir/covdata/$profile_dir -name '*.profraw' | $nop xargs $LLVM_PROFDATA merge --sparse \
+ -o $profdata
+ $nop $LLVM_COV show -Xdemangler=$HOME/.cargo/bin/rustfilt '--ignore-filename-regex=/tests/' '--ignore-filename-regex=/tests\.rs' '--ignore-filename-regex=\.cargo' --format=html \
+ -o $abs_builddir/covdata/html/$profile --instr-profile=$profdata \
+ ${=${(j: --object :)test_objects}}
+}
+
+check-litter() {
+ setopt LOCAL_OPTIONS NULL_GLOB
+ typeset litter=($testdir/*.actual)
+ (( $#litter > 0 )) || return 0
+ print -u2 "$1 littered $litter"
+ false
+}
+
+report_only=false
+coverage=false
+typeset -a actions profiles
+while [[ $# -gt 0 ]]; do
+ word=$1
+ shift
+ case $word in
+ -n)
+ nop=(print --)
+ ;;
+
+ -cover*)
+ coverage=:
+ if [[ $word = -cover-report ]]; then
+ report_only=:
+ else
+ $nop rm -rf $abs_builddir/covdata
+ fi
+ . ~/.cargo/env
+ export RUSTFLAGS="-Zinstrument-coverage $RUSTFLAGS"
+ ;;
+
+ all)
+ # We test implicit TLS with Gmail and explicit with Gandi; any reason to test both ways both places?
+ set -- cargot test live/panda live/dovecot live/cyrus live/gandi-starttls live/gmail "$@"
+ ;;
+
+ build|clean)
+ actions+=$word
+ ;;
+
+ cargot|test|live/*)
+ actions+=$word
+ profiles+=$word
+ ;;
+
+ test/*)
+ actions+=$word
+ profiles+=test
+ ;;
+
+ *)
+ print -u2 "get outta here $word"
+ exit 3
+ ;;
+ esac
+done
+
+if ! $report_only; then
+for action in $actions; do
+ case $action in
+ build)
+ (cd $abs_builddir && $nop $MAKE $MAKE_JOBS all cargo)
+ ;;
+
+ cargot)
+ $nop env LLVM_PROFILE_FILE="$abs_builddir/covdata/cargot/%m-%p.profraw" cargo t -- --test-threads=1
+ ;;
+
+ test)
+ tests=(
+ test/imap/test-astring
+ test/imap/test-bad
+ test/imap/test-bad-fetch-body
+ test/imap/test-folder-create-disallowed
+ # Downloads all mail from your test account; better be sure you want that!
+ # test/imap/test-gmail-all
+ test/imap/test-inc
+ test/imap/test-inc-missing-msg
+ test/imap/test-inc-missing-msg-case-2
+ # Handled specially below.
+ # test/imap/test-live
+ test/imap/test-login
+ test/imap/test-mark
+ test/imap/test-quirks-gmail
+ # TODO fix the bitrot
+ # test/imap/test-refile
+ test/imap/test-rmm
+ test/imap/test-scan
+ test/imap/test-server-drops
+ test/imap/test-uidvalidity
+ test/imap/test-xoauth
+ )
+ (cd $abs_builddir && for test in $tests; do
+ $nop env PANDA_IMAPD=$PANDA_IMAPD MH_COVERAGE_PROFILE=test $MAKE check TESTS=$test
+ check-litter $test
+ done)
+ ;;
+
+ test/*)
+ (cd $abs_builddir && $nop env PANDA_IMAPD=$PANDA_IMAPD MH_COVERAGE_PROFILE=test $MAKE check TESTS=$action)
+ check-litter $action
+ ;;
+
+ live/panda)
+ (cd $abs_builddir && $nop env PANDA_IMAPD=$PANDA_IMAPD MH_COVERAGE_PROFILE=live/panda MH_IMAP_TEST_SERVER=panda \
+ $MAKE check TESTS=test/imap/test-live)
+ check-litter $action
+ ;;
+
+ live/dovecot)
+ (cd $abs_builddir && $nop env MH_COVERAGE_PROFILE=live/dovecot MH_IMAP_TEST_SERVER=127.0.0.1 MH_IMAP_TEST_PORT=1043 \
+ MH_IMAP_TEST_USER=test MH_IMAP_TEST_PASSWORD=testpw MH_IMAP_TEST_EMAIL_ADDRESS=test MH_IMAP_TEST_TLS=no \
+ MH_IMAP_SERVER_DRAFT_BORN_UNSEEN=' ' MH_IMAP_SERVER_EATS_TRAILING_EOL=' ' \
+ $MAKE check TESTS=test/imap/test-live)
+ check-litter $action
+ ;;
+
+ live/cyrus)
+ (cd $abs_builddir && $nop env MH_COVERAGE_PROFILE=live/cyrus MH_IMAP_TEST_SERVER=127.0.0.1 MH_IMAP_TEST_PORT=143 MH_IMAP_TEST_USER=test \
+ MH_IMAP_TEST_PASSWORD=test MH_IMAP_TEST_EMAIL_ADDRESS=test MH_IMAP_TEST_TLS=no MH_IMAP_TEST_MAILBOX_ROOT=test/ \
+ MH_IMAP_SERVER_DRAFT_BORN_UNSEEN='U' \
+ $MAKE check TESTS=test/imap/test-live)
+ check-litter $action
+ ;;
+
+ # Looks like Gandi use Dovecot
+ live/gandi)
+ (cd $abs_builddir && $nop env MH_COVERAGE_PROFILE=live/gandi MH_IMAP_TEST_SERVER=mail.gandi.net MH_IMAP_TEST_PORT=imaps \
+ MH_IMAP_TEST_USER=$gandi_user MH_IMAP_TEST_PASSWORD=$gandi_pw MH_IMAP_TEST_EMAIL_ADDRESS=$gandi_user \
+ MH_IMAP_TEST_TLS=implicit MH_IMAP_TEST_MAILBOX_ROOT=test/ \
+ MH_IMAP_SERVER_DRAFT_BORN_UNSEEN=' ' \
+ $MAKE check TESTS=test/imap/test-live)
+ check-litter $action
+ ;;
+
+ live/gandi-starttls)
+ (cd $abs_builddir && $nop env MH_COVERAGE_PROFILE=live/gandi-starttls MH_IMAP_TEST_SERVER=mail.gandi.net MH_IMAP_TEST_PORT=imap \
+ MH_IMAP_TEST_USER=$gandi_user MH_IMAP_TEST_PASSWORD=$gandi_pw MH_IMAP_TEST_EMAIL_ADDRESS=$gandi_user \
+ MH_IMAP_TEST_TLS=explicit MH_IMAP_TEST_MAILBOX_ROOT=test/ \
+ MH_IMAP_SERVER_DRAFT_BORN_UNSEEN=' ' \
+ $MAKE check TESTS=test/imap/test-live)
+ check-litter $action
+ ;;
+
+ live/gmail)
+ (cd $abs_builddir && $nop env MH_COVERAGE_PROFILE=live/gmail MH_IMAP_TEST_SERVER=gmail MH_IMAP_TEST_PORT=imaps MH_IMAP_TEST_USER=$gmail_user \
+ MH_IMAP_TEST_OAUTH_FILE=$gmail_oauth MH_IMAP_TEST_EMAIL_ADDRESS=$gmail_user MH_IMAP_TEST_MAILBOX_ROOT=test/ \
+ MH_IMAP_SERVER_DRAFT_BORN_UNSEEN='U' \
+ $MAKE check TESTS=test/imap/test-live)
+ check-litter $action
+ ;;
+
+ report-coverage)
+ ;;
+
+ clean)
+ $nop rm -rf $abs_builddir/target
+ $nop rm -rf $testdir
+ ;;
+ esac
+done
+fi
+
+if $coverage; then
+ test_objects=(
+ $abs_builddir/target/debug/imap-folder-manager
+ $($nop env LLVM_PROFILE_FILE="$abs_builddir/covdata/cargot/%m-%p.profraw" cargo t --tests --no-run --message-format=json 2>/dev/null \
+ | $nop perl -MJSON::PP -lne 'eval { $item = decode_json($_) }; if ($@) { @all=(); die($@) } $fns = $item->{profile}->{test} && $item->{filenames}; $fns && push(@all, @$fns); END { print join("\n", @all) }')
+ )
+ print -l $test_objects
+ cd $abs_srcdir
+ report-coverage all
+ (( ${profiles[(i)live/*]} <= ${#profiles} )) && report-coverage live live
+ for profile in $profiles; do
+ report-coverage $profile $profile
+ done
+ $nop chmod -R a+rX $abs_builddir/covdata/html
+ exit
+fi
+++ /dev/null
-# guide file for acme(1)
-
-make clean make ctags 'CTAGS=atags'
-sh autogen.sh
-./configure '--prefix=/usr/local'
-
-tags grep -rn .
-
-git diff git status git pull
--- /dev/null
+/* folder.h -- mh_folder and associated operations..
+ *
+ * TODO - Replace most (all?) direct use of folder_read, m_convert, etc. with
+ * common operations. To start with, move each command's own version in here.
+ * Once they're all factored out, we can factor out commonalities and reduce to
+ * the smallest set of common operations.
+ *
+ * Operations are:
+ * - Open
+ * - Read
+ * - Select
+ * - ???
+ * - Close
+ */
+
+typedef struct mh_folder mh_folder;
+
+bool
+mh_folder_open(mh_folder **result, char const *name, mh_managed_folders *mmfs);
+
+char const *
+mh_folder_get_err_string(mh_folder const *mhf);
+
+void
+mh_folder_close(mh_folder *mhf);
+
+/* TODO Factor just a few common message selection routines out of all these
+ * after message selection has been factored out of all uip programs. */
+
+// TODO Do we need to chdir?
+struct msgs const *
+mh_folder_select_and_chdir_for_scan(mh_folder *mhf, struct msgs_array msgs);
+bool
+mh_folder_message_is_unseen_for_scan(mh_folder *mhf, int msgnum);
+
+struct msgs const *
+mh_folder_select_and_chdir_for_send(mh_folder *mhf, char *const *msgs, size_t msg_count);
+
+struct msgs const *
+mh_folder_select_and_chdir_and_mark_seen(struct msgs_array *vec, mh_folder *mhf, struct msgs_array msgs);
--- /dev/null
+
+// TODO opaque (or just replace with the rust implementation)
+typedef struct {
+ char name[NAMESZ], value_buf[BUFSIZ];
+ m_getfld_state_t getfld_ctx;
+} mh_folder_manager_protocol_read_message_t;
+
+bool
+mh_folder_manager_protocol_read_message(svector_t, mh_folder_manager_protocol_read_message_t *);
+
+bool
+mh_folder_manager_protocol_write_message(FILE *fp, char const *message, size_t partial_len);
+
+/* TODO out of place */
+void
+free_svector_and_elements(svector_t *);
--- /dev/null
+typedef struct mh_managed_folders mh_managed_folders;
+
+typedef struct mh_managed_folder mh_managed_folder;
+
+// TODO separate .h / .c file for mh_folder_manager_*?
+typedef struct mh_folder_manager_client mh_folder_manager_client;
+typedef struct mh_folder_manager_inc_context mh_folder_manager_inc_context;
+
+/*
+ * On error, return false and set an error in mmfs; mmfs is always allocated.
+ */
+bool
+mh_managed_folders_open(mh_managed_folders **mmfs);
+
+/*
+ * Return null-terminated error message after some function indicated an error.
+ *
+ * Never returns NULL, but must not be called if an error was not indicated.
+ * Return value must be freed ONLY via mh_managed_folders_free.
+ */
+char const *
+mh_managed_folders_get_err_string(mh_managed_folders *);
+
+char const *
+mh_managed_folder_get_err_string(mh_managed_folder *);
+
+/*
+ * Return managed folder information if folder is managed, else NULL.
+ */
+mh_managed_folder *
+mh_managed_folder_lookup(mh_managed_folders const *mmfs, char const *name, size_t name_len);
+
+char const *
+mh_managed_folder_manager_path(mh_managed_folder const *mmf);
+
+char const *
+mh_managed_folder_socket_path(mh_managed_folder const *mmf);
+
+void
+mh_managed_folders_free(mh_managed_folders *mmfs);
+
+char const *
+mh_managed_folder_name(mh_managed_folder const *mmf);
+
+bool
+mh_folder_manager_connect(mh_folder_manager_client **mfmc, mh_managed_folder *mmf);
+
+bool
+mh_folder_manager_close(mh_folder_manager_client *mfmc);
+
+int
+mh_folder_manager_create_draft(
+ mh_folder_manager_client *mfmc,
+ char const *draft_folder);
+
+bool
+mh_folder_manager_make_folder(
+ mh_folder_manager_client *mfmc,
+ char const *folder);
+
+bool
+mh_folder_manager_update_draft(
+ mh_folder_manager_client *mfmc,
+ char const *draft_folder,
+ int msgnum);
+
+/*
+ * Returns NULL on error.
+ */
+mh_folder_manager_inc_context *
+mh_folder_manager_inc(mh_folder_manager_client *mfmc, char const *folder);
+
+bool
+mh_folder_manager_folder_read(mh_folder_manager_client *mfmc, char const *dfolder);
+
+bool
+mh_folder_manager_refile_move(
+ mh_folder_manager_client *mfmc,
+ char const *src_folder,
+ struct msgs_array msgs,
+ char const *dst_folder);
+
+bool
+mh_folder_manager_refile_file(
+ mh_folder_manager_client *mfmc,
+ char const *src_file,
+ char const *dst_folder);
+
+typedef struct {
+ bool done;
+ char *folder;
+ int msg;
+ bool unseen;
+} mh_managed_folder_inc_page;
+
+/*
+ * Return NULL on error.
+ */
+mh_managed_folder_inc_page *
+mh_folder_manager_inc_next(mh_folder_manager_inc_context *mfinc);
+
+/* Ask folder manager to pick messages specified by `msgs`.
+ *
+ * Folder manager writes picked messages into the special sequence 'all'.
+ *
+ * Ask the folder manager to set the unseen sequence if `need_unseen` is 1 and to delete it if 0.
+ * Any value other than 0 or 1 is an error.
+ */
+bool
+mh_folder_manager_pick(mh_folder_manager_client *mfmc, char const *folder, struct msgs_array msgs, int need_unseen);
+
+bool
+mh_folder_manager_rmf(mh_folder_manager_client *mfmc, char const *folder);
+
+bool
+mh_folder_manager_rmm(mh_folder_manager_client *mfmc, char const *folder, struct msgs_array msgs);
+
+bool
+mh_folder_manager_mark_add(
+ mh_folder_manager_client *mfmc,
+ char const *folder,
+ struct msgs_array msgs,
+ struct svector const *sequences);
+
+bool
+mh_folder_manager_mark_del(
+ mh_folder_manager_client *mfmc,
+ char const *folder,
+ struct msgs_array msgs,
+ struct svector const *sequences);
+
+void
+mh_managed_folder_set_err(mh_managed_folder *mmf, char const *error_message);
* If refreshing, writes freshened tokens to disk.
*
* Exits via adios on any error.
- *
- * Always returns OK for now, but in the future could return NOTOK on error.
*/
-
-int
+void
mh_oauth_do_xoauth(const char *user, const char *svc, unsigned char **oauth_res,
size_t *oauth_res_len, FILE *log);
--- /dev/null
+[package]
+name = "imap-folder-manager"
+# TODO derive from nmh version
+version = "0.0.0"
+edition = "2018"
+
+[dependencies]
+libc = "0.2.78"
+log = { version = "0.4", features = ["std"] }
+# nmh
+managed-folders = { path = "../../managedfolders" }
+nmh = { path = "../../sbr/rust" }
+nmhimapstore = { path = "../store" }
+nmhimaptypes = { path = "../types" }
--- /dev/null
+use std::num::NonZeroU32;
+use std::path::{Path, PathBuf};
+
+use nmh::{netsec, profile, Netsec};
+
+use crate::mailbox_rule::MailboxRules;
+use crate::regex::Regex;
+
+/// Short-lived configuration only needed for startup.
+pub struct Init<'a> {
+ pub socket_path: &'a Path,
+ pub auth: Auth,
+}
+
+impl<'a> Init<'a> {
+ pub fn new(prefixed: &'a profile::Prefixed) -> Result<Self, String> {
+ Ok(Self {
+ socket_path: socket_path(prefixed.get_or_err("socket")?)?,
+ auth: Auth::new(prefixed)?,
+ })
+ }
+}
+
+/// [crate::manager::Manager]'s configuration.
+#[derive(Debug)]
+pub struct Manager {
+ pub db_path: PathBuf,
+ pub mailbox_root: String,
+ pub max_fetch_messages_in_flight: NonZeroU32,
+ pub rules: crate::mailbox_rule::MailboxRules,
+}
+
+impl Manager {
+ pub fn new(prefixed: &profile::Prefixed) -> Result<Self, String> {
+ Ok(Self {
+ db_path: Path::new(prefixed.get_or_err("db")?).to_owned(),
+ mailbox_root: prefixed.get("mailbox-root").unwrap_or("").trim().to_owned(),
+ // TODO Reasonable defaults? Or make the user pick?
+ max_fetch_messages_in_flight: match prefixed.get("max-fetch-messages-in-flight") {
+ Some(s) => s.trim().parse::<NonZeroU32>().map_err(|_| {
+ "{}-max-fetch-messages-in-flight must be non-zero unsigned 32-bit integer"
+ })?,
+ None => NonZeroU32::new(100).expect("100 is non-zero"),
+ },
+ rules: mailbox_rules(prefixed.iter()),
+ })
+ }
+}
+
+pub enum Auth {
+ PREAUTH(netsec::Builder, nmh::client::Connector),
+ SASL(netsec::Builder, TLS, nmh::client::Connector),
+}
+
+impl Auth {
+ pub fn new(prefixed: &profile::Prefixed) -> Result<Self, String> {
+ let connector = nmh::client::Connector::new()
+ .server(prefixed.get_or_err("host")?)?
+ .service(prefixed.get_or_err("port")?)?;
+ let tls_type: TLS = prefixed.get_or_err("tls")?.parse()?;
+ let auth_type = prefixed.get_or_err("auth")?.trim();
+ Ok(match auth_type {
+ "PREAUTH" => {
+ match tls_type {
+ TLS::Nah => {}
+ _ => todo!("PREAUTH only implemented for tests; good enough?"),
+ }
+ Self::PREAUTH(Netsec::builder().snoop(), connector)
+ }
+ "SASL" => Self::SASL(
+ {
+ let mut builder = Netsec::builder();
+ if let Some(oauth_service) = prefixed.get("authservice") {
+ builder.sasl_oauth_service(oauth_service.trim())?
+ }
+ if let Some(mech) = prefixed.get("saslmech") {
+ builder.sasl_prefer_mechanism(mech.trim())?
+ }
+ builder
+ .sasl_service("imap")?
+ .sasl_userid(prefixed.get_or_err("user")?)?
+ .host(connector.get_server())
+ .snoop()
+ },
+ tls_type,
+ connector,
+ ),
+ _ => todo!("unknown auth parameter: {}", auth_type),
+ })
+ }
+}
+
+pub enum TLS {
+ Explicit,
+ Implicit,
+ Nah,
+ Yolo,
+ // TODO skipping certificate verification is orthogonal to explicit vs. implicit
+ ExplicitYolo,
+}
+
+impl std::str::FromStr for TLS {
+ type Err = String;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s {
+ "explicit" => Ok(Self::Explicit),
+ "implicit" => Ok(Self::Implicit),
+ "don't protect my data i know what i'm doing" => Ok(Self::Nah),
+ "implicit but don't verify the cert i already did out of band" => Ok(Self::Yolo),
+ "explicit but don't verify the cert i already did out of band" => {
+ Ok(Self::ExplicitYolo)
+ }
+ _ => Err(format!("unknown TLS configuration {}", s)),
+ }
+ }
+}
+
+fn socket_path(s: &str) -> Result<&Path, String> {
+ // TODO(rust-lang#62061): const std::mem::zeroed
+ let sun: libc::sockaddr_un = unsafe { std::mem::zeroed() };
+ let len = sun.sun_path.len() - 1;
+ if s.len() > len {
+ Err(format!("socket path {} > system limit of {} bytes", s, len))
+ } else {
+ Ok(Path::new(s))
+ }
+}
+
+fn mailbox_rules(iter: profile::PrefixedIterator) -> MailboxRules {
+ let mut rules = MailboxRules::new();
+ for (key, value) in iter {
+ if key == "mailbox-include" {
+ rules.include(Regex::compile(value))
+ }
+ if key == "mailbox-exclude" {
+ rules.exclude(Regex::compile(value))
+ }
+ }
+ rules
+}
--- /dev/null
+//! Support for recovering from errors.
+//!
+//! We use methods consuming the `self` parameter for safety, but we want to
+//! recover from some errors. Such methods can return
+//! `Result<(Data, T), RecoverableError<T>>`.
+
+pub struct RecoverableError<T: std::fmt::Debug, E: 'static + std::error::Error> {
+ recoverable: Option<T>,
+ source: Box<E>,
+}
+
+impl<T: std::fmt::Debug, E: 'static + std::error::Error> RecoverableError<T, E> {
+ /// Return an `Error` wrapping `source` and holding `recoverable`, which may be [RecoverableError::recover]ed.
+ pub fn recoverable(source: E, recoverable: T) -> Self {
+ Self {
+ recoverable: Some(recoverable),
+ source: Box::new(source),
+ }
+ }
+
+ /// Return an `Error` wrapping `source` from which nothing may be [RecoverableError::recover]ed.
+ pub fn unrecoverable(source: E) -> Self {
+ Self {
+ recoverable: None,
+ source: Box::new(source),
+ }
+ }
+
+ /// Return a tuple of the `(recovered, source)` parameters of [RecoverableError::recoverable] or
+ /// the `source` parameter of [RecoverableError::unrecoverable]. Callers who must return their
+ /// own [RecoverableError] may benefit from [RecoverMapper].
+ pub fn recover(self) -> Result<(T, E), E> {
+ match self.recoverable {
+ None => Err(*self.source),
+ Some(recovered) => Ok((recovered, *self.source)),
+ }
+ }
+}
+
+impl<T: std::fmt::Debug, E: 'static + std::error::Error> std::fmt::Debug
+ for RecoverableError<T, E>
+{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}recoverable: {:?}",
+ if self.recoverable.is_none() { "un" } else { "" },
+ self.source
+ )
+ }
+}
+
+impl<T: std::fmt::Debug, E: 'static + std::error::Error> std::fmt::Display
+ for RecoverableError<T, E>
+{
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.source.fmt(f)
+ }
+}
+
+impl<T: std::fmt::Debug, E: 'static + std::error::Error> std::error::Error
+ for RecoverableError<T, E>
+{
+ fn source(&self) -> Option<&(dyn 'static + std::error::Error)> {
+ Some(self.source.as_ref())
+ }
+}
+
+/// Helps consumers of [RecoverableError] of one recovered type map to their own recovered type.
+///
+/// Only good for one use.
+/// Either [RecoverMapper::map] or [RecoverMapper::ok] may be called, and only once.
+///
+/// ```
+/// rm = RecoverMapper::new(&|foo, foo_holder| foo_holder.rebuild(foo), foo_holder);
+/// // on error, map RecoverableError<Foo, E> to RecoverableError<FooHolder, E>
+/// foo.fallible().map_err(|e| rm.map(e))?;
+/// // success case
+/// foo_holder = rm.ok();
+/// ```
+pub struct RecoverMapper<'a, From, To, Context>
+where
+ From: std::fmt::Debug,
+ To: std::fmt::Debug,
+{
+ context: Option<Context>,
+ mapper: &'a dyn Fn(From, Context) -> To,
+ _p1: std::marker::PhantomData<From>,
+ _p2: std::marker::PhantomData<To>,
+}
+
+impl<'a, From, To, Context> RecoverMapper<'a, From, To, Context>
+where
+ From: std::fmt::Debug,
+ To: std::fmt::Debug,
+{
+ /// Create a `RecoverMapper` whose [RecoverMapper::map] method calls `mapper(e, context)` where
+ /// `e` is the parameter to [RecoverMapper::map] and `context` is the parameter here.
+ pub fn new(mapper: &'a dyn Fn(From, Context) -> To, context: Context) -> Self {
+ Self {
+ context: Some(context),
+ mapper,
+ _p1: std::marker::PhantomData,
+ _p2: std::marker::PhantomData,
+ }
+ }
+
+ /// Map one RecoverableError to another using `mapper` supplied to [RecoverMapper::new].
+ /// Panic if called twice, or after a call to [RecoverMapper::ok].
+ pub fn map<E: 'static + std::error::Error>(
+ &mut self,
+ from: RecoverableError<From, E>,
+ ) -> RecoverableError<To, E> {
+ match from.recoverable {
+ None => RecoverableError::unrecoverable(*from.source),
+ Some(recovered) => RecoverableError::recoverable(
+ *from.source,
+ (self.mapper)(recovered, self.context.take().expect("RecoverMapper reuse")),
+ ),
+ }
+ }
+
+ /// Take back the `context` passed to [RecoverMapper::new].
+ /// Panic if called twice, or after a call to [RecoverMapper::map].
+ pub fn ok(self) -> Context {
+ self.context.expect("RecoverMapper::ok called after error")
+ }
+}
--- /dev/null
+//! IMAP
+//!
+//! RFC 3501 defines these states:
+//! - 3.1. `Not Authenticated State`
+//! We start here. Use [auth::login] to move to...
+//! - 3.2. `Authenticated State`
+//! Modeled by [Session].
+//! - 3.3. `Selected State`
+//! Modeled by [mailbox::Session].
+//! Entered via [Session::select].
+//! Return to `Authenticated State` with [mailbox::Session::close].
+//! - 3.4. `Logout State`
+//! Entered when we say `LOGOUT` in `Drop` or server says `BYE`. Either way, we
+//! close the connection in `Drop`.
+
+pub mod auth;
+pub mod mailbox;
+mod protocol;
+mod session;
+
+pub use session::{Error, FetchResult, MessageReader, MessageResult, Session};
+
+pub enum Flag {
+ /// `\Deleted`
+ Deleted,
+ /// `\Draft`
+ Draft,
+ /// `\Seen`
+ Seen,
+ // Custom(String) ?
+}
+
+impl std::fmt::Display for Flag {
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ formatter.write_str(match self {
+ Self::Deleted => "\\Deleted",
+ Self::Draft => "\\Draft",
+ Self::Seen => "\\Seen",
+ })
+ }
+}
+
+/// Returned by [Session::list].
+#[derive(Eq, Ord, PartialEq, PartialOrd)]
+pub struct Mailbox {
+ pub name: mailbox::Name,
+ /// Whether [Session::select] and [mailbox::Session::select] will work with this mailbox.
+ pub selectable: bool,
+}
--- /dev/null
+use nmh::{netsec, Error, Netsec};
+
+use crate::cf;
+use crate::imap::protocol::{
+ read_greeting, read_response, Capabilities, Capability, Greeting, ReadResult, Response,
+ ResponseCode, ServerData, Status,
+};
+use crate::imap::Session;
+
+type Result<T> = std::result::Result<T, Error>;
+
+// TODO Instead have Netsec and netsec::Authenticator implement a trait?
+macro_rules! maybe_read_capabilities_response {
+ ($name:expr, $netsec:expr) => {{
+ let mut capabilities = None;
+ loop {
+ let response: &str = $netsec.read_line()?;
+ match read_response(response)? {
+ ReadResult::Response(response) => match response {
+ Response::ServerData(ServerData::Capability(c)) => capabilities = Some(c),
+ Response::TaggedStatus(_, status) => match status {
+ Status::Ok(code) => {
+ if let ResponseCode::Capability(c) = code {
+ capabilities = Some(c)
+ }
+ break;
+ }
+ other => todo!("only handling OK: {:?}", other),
+ },
+ _ => {}
+ },
+ _ => todo!("unexpected response to {}: {}", $name, response),
+ }
+ }
+ capabilities
+ }};
+}
+macro_rules! request_capabilities_if_none {
+ ($opt_capabilities:expr, $netsec:expr) => {
+ match $opt_capabilities {
+ Some(capabilities) => capabilities,
+ None => {
+ $netsec.write_and_flush(b"1 CAPABILITY\r\n")?;
+ maybe_read_capabilities_response!("CAPABILITY", $netsec)
+ .ok_or_else(|| Error::new("server claims no capabilities"))?
+ }
+ }
+ };
+}
+
+// TODO allow config to override capabilities in case of broken server?
+pub fn login(context: &nmh::Context, auth_type: cf::Auth) -> Result<Session> {
+ let (mut netsec, opt_capabilities) = match auth_type {
+ cf::Auth::PREAUTH(builder, connector) => preauth(builder, connector)?,
+ cf::Auth::SASL(builder, tls, connector) => sasl(context, builder, tls, connector)?,
+ };
+ let capabilities = request_capabilities_if_none!(opt_capabilities, &mut netsec);
+ Session::new(netsec, capabilities)
+}
+
+fn preauth(
+ builder: netsec::Builder,
+ connector: nmh::client::Connector,
+) -> Result<(Netsec, Option<Capabilities>)> {
+ let mut netsec: Netsec = builder.connect(connector)?.skip_tls().skip_sasl();
+ let capabilities;
+ let greeting: &mut str = netsec.read_line()?;
+ match read_greeting(greeting)? {
+ Greeting::PreAuth(code) => match code {
+ ResponseCode::Capability(c) => capabilities = Some(c),
+ _ => capabilities = None,
+ },
+ _ => todo!("PREAUTH requested but server greeting is {}", greeting),
+ }
+ Ok((netsec, capabilities))
+}
+
+fn sasl(
+ context: &nmh::Context,
+ mut builder: netsec::Builder,
+ tls: crate::cf::TLS,
+ connector: nmh::client::Connector,
+) -> Result<(Netsec, Option<Capabilities>)> {
+ let unauthenticated_capabilities;
+ let mut netsec_auth: netsec::sasl::Authenticator = match tls {
+ cf::TLS::Explicit | cf::TLS::ExplicitYolo => {
+ if let cf::TLS::ExplicitYolo = tls {
+ builder = builder.skip_certificate_checks()
+ }
+ let mut nc: netsec::tls::Connector = builder.connect(connector)?;
+ let pre_tls_capabilities =
+ request_capabilities_if_none!(read_ok_greeting(nc.read_line()?)?, &mut nc);
+ if !pre_tls_capabilities.has(Capability::StartTLS) {
+ return Err(Error::new("server doesn't offer STARTTLS"));
+ }
+ nc.write_and_flush(b"1 STARTTLS\r\n")?;
+ unauthenticated_capabilities = maybe_read_capabilities_response!("STARTTLS", nc);
+ nc.negotiate_tls()?
+ }
+ cf::TLS::Implicit | cf::TLS::Yolo => {
+ if let cf::TLS::Yolo = tls {
+ builder = builder.skip_certificate_checks()
+ }
+ let mut netsec_auth: netsec::sasl::Authenticator =
+ builder.connect(connector)?.negotiate_tls()?;
+ unauthenticated_capabilities = read_ok_greeting(netsec_auth.read_line()?)?;
+ netsec_auth
+ }
+ cf::TLS::Nah => {
+ let mut netsec_auth: netsec::sasl::Authenticator =
+ builder.connect(connector)?.skip_tls();
+ unauthenticated_capabilities = read_ok_greeting(netsec_auth.read_line()?)?;
+ netsec_auth
+ }
+ };
+ let unauthenticated_capabilities =
+ request_capabilities_if_none!(unauthenticated_capabilities, &mut netsec_auth);
+ if !unauthenticated_capabilities.has(Capability::SASLIR) {
+ return Err(Error::new(
+ "server doesn't offer SASL-IR; let us know and we'll support this configuration",
+ ));
+ }
+ let mut callback = Callback(None);
+ let netsec = netsec_auth
+ .server_supported_sasl_mechanisms(unauthenticated_capabilities.iter().filter_map(|cap| {
+ match cap {
+ Capability::Auth(mech) => Some(*mech),
+ _ => None,
+ }
+ }))
+ .negotiate_sasl(context, &mut callback)?;
+ Ok((netsec, callback.0))
+}
+
+struct Callback(Option<Capabilities>);
+
+impl netsec::sasl::Callback<Error> for Callback {
+ fn start(
+ &mut self,
+ netsec: &mut Netsec,
+ selected_mechanism: &str,
+ initial_client_response: &[u8],
+ ) -> netsec::sasl::Result<(), Error> {
+ if initial_client_response.is_empty() {
+ todo!("empty sasl start initial_client_response not implemented")
+ }
+ let b64data: nmh::str::NString = nmh::base64::encode_raw(initial_client_response);
+ // TODO does anything NOT have SASL-IR capability? https://www.imapwiki.org/Specs says maybe Courier
+ Ok(netsec.write_with_base64_at_offset_then_crlf_then_flush(
+ &format!("1 AUTHENTICATE {} {}\r\n", selected_mechanism, b64data),
+ // TODO 16 is len("1 AUTHENTICATE ")
+ 16 + selected_mechanism.len() as std::os::raw::c_int,
+ )?)
+ }
+
+ fn read(&mut self, _: &mut Netsec) -> netsec::sasl::Result<Vec<u8>, Error> {
+ todo!("NETSEC_SASL_READ")
+ }
+
+ fn write(&mut self, _netsec: &mut Netsec, _message: &[u8]) -> netsec::sasl::Result<(), Error> {
+ todo!("NETSEC_SASL_WRITE")
+ // if !message.is_empty() {
+ // todo!("NETSEC_SASL_WRITE");
+ // }
+ // netsec.write(b"\r\n")?;
+ // Ok(netsec.flush()?)
+ }
+
+ fn finish(&mut self, netsec: &mut Netsec) -> netsec::sasl::Result<(), Error> {
+ self.0 = maybe_read_capabilities_response!("AUTHENTICATE", netsec);
+ Ok(())
+ }
+
+ fn cancel(&mut self) -> netsec::sasl::Result<(), Error> {
+ todo!("NETSEC_SASL_CANCEL not implemented")
+ }
+}
+
+fn read_ok_greeting(greeting: &mut str) -> Result<Option<Capabilities>> {
+ Ok(match read_greeting(greeting)? {
+ Greeting::Ok(code) => match code {
+ ResponseCode::Capability(capabilities) => Some(capabilities),
+ _ => None,
+ },
+ _ => todo!("unexpected greeting {}", greeting),
+ })
+}
--- /dev/null
+pub use super::session::mailbox::{Error, Selected, Session};
+
+// TODO Support hierarchy separator other than '/'? Cyrus defaults to '.'
+#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
+pub struct Name(String);
+
+impl Name {
+ pub fn as_str(&self) -> &str {
+ self.0.as_str()
+ }
+}
+
+impl std::fmt::Display for Name {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl From<String> for Name {
+ fn from(s: String) -> Self {
+ Name(s)
+ }
+}
--- /dev/null
+//! IMAP protocol handling used by the high-level [crate::imap::Session].
+
+use log::{debug, trace, warn};
+
+use nmh::Error;
+use nmhimaptypes::{uid, Uid, UidValidity};
+
+mod response;
+pub use response::{
+ Capabilities, Capability, Fetch, Greeting, Response, ResponseCode, ServerData, Status,
+};
+
+type Result<T> = std::result::Result<T, Error>;
+
+// greeting = "*" SP ("OK" / "PREAUTH" / "BYE" SP resp-text) CRLF
+pub fn read_greeting(line: &mut str) -> Result<Greeting> {
+ trace!("read_greeting({})", line);
+ // TODO de-duplicate with read_response / read_untagged
+ let (first_word, index_of_rest): (&str, usize) = {
+ let first_space: usize = line
+ .find(' ')
+ .ok_or_else(|| Error::new("protocol error: no space in response"))?;
+ (&line[..first_space], first_space + 1)
+ };
+ match first_word {
+ "*" => {}
+ _ => {
+ return Err(Error::new(format!(
+ "protocol error: no leading * in {}",
+ line
+ )))
+ }
+ }
+ let mut index = line[index_of_rest..]
+ .find(' ')
+ .ok_or_else(|| Error::new("protocol error: resp-text missing from resp-cond-auth"))?;
+ index += index_of_rest;
+ line[index_of_rest..index].make_ascii_uppercase();
+ let word = &line[index_of_rest..index];
+ let code: ResponseCode = read_resp_text(&line[index + 1..])?.0;
+ match word {
+ "OK" => Ok(Greeting::Ok(code)),
+ "PREAUTH" => Ok(Greeting::PreAuth(code)),
+ "BYE" => todo!("BYE in greeting"),
+ _ => todo!("protocol error: unexpected greeting {} in {}", word, line),
+ }
+}
+
+pub fn read_response(line: &str) -> Result<ReadResult> {
+ let (first_word, index_of_rest): (&str, usize) = {
+ let first_space: usize = line
+ .find(' ')
+ .ok_or_else(|| Error::new("protocol error: no space in response"))?;
+ (&line[..first_space], first_space + 1)
+ };
+ match first_word {
+ "+" => Ok(ReadResult::CommandContinuationRequest),
+ "*" => read_untagged(&line[index_of_rest..]),
+ _ => Ok(ReadResult::Response(Response::TaggedStatus(
+ first_word.to_owned(),
+ read_resp_cond_state(&line[index_of_rest..])?,
+ ))),
+ }
+}
+
+/// The result of any IMAP read operation, such as [read_response].
+pub enum ReadResult {
+ /// Server is awaiting client to send a continuation request.
+ CommandContinuationRequest,
+ /// Server has provided a response.
+ Response(Response),
+ // TODO Should Vec<Fetch> show up here?. Better would be to stash it inside the LineReader?
+ // we're not returning anything out of that in this case right now anyway.
+ /// Server has this many bytes for client to read. Caller must read them and then read a line and pass to `LineReader`.
+ FetchBytes(Vec<Fetch>, usize, LineReader),
+ /// Server has sent a partial response and caller must read this many bytes and pass bo `ByteReader` in order to continue.
+ NeedBytes(usize, ByteReader),
+ /// Server has sent a partial response and caller must read one line and pass bo `LineReader` in order to continue.
+ NeedLine(LineReader),
+}
+
+impl std::fmt::Debug for ReadResult {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ReadResult::CommandContinuationRequest => {
+ f.debug_tuple("CommandContinuationRequest").finish()
+ }
+ ReadResult::Response(r) => f
+ .debug_tuple("Response")
+ .field(&format!("{:?}", r))
+ .finish(),
+ ReadResult::FetchBytes(_, n, reader) => f
+ .debug_tuple("FetchBytes")
+ .field(&"Vec<Fetch>")
+ .field(n)
+ .field(&format!("{:?}", reader))
+ .finish(),
+ ReadResult::NeedBytes(n, reader) => f
+ .debug_tuple("NeedBytes")
+ .field(n)
+ .field(&format!("{:?}", reader))
+ .finish(),
+ ReadResult::NeedLine(reader) => f
+ .debug_tuple("NeedLine")
+ .field(&format!("{:?}", reader))
+ .finish(),
+ }
+ }
+}
+
+pub struct ByteReader(Option<ByteReading>);
+
+enum ByteReading {
+ MailboxList(MailboxListReader),
+}
+
+impl ByteReader {
+ fn new(reading: ByteReading) -> Self {
+ Self(Some(reading))
+ }
+
+ pub fn read_bytes(mut self, bytes: &[u8]) -> Result<ReadResult> {
+ match self.0.take().expect("ByteReader not reusable") {
+ ByteReading::MailboxList(reader) => reader.read_bytes(bytes),
+ }
+ }
+
+ // TODO implement this for use when caller decides to LOGOUT rather than continue
+ // pub fn abort(mut self) {
+ // self.0 = None;
+ // }
+}
+
+impl Drop for ByteReader {
+ fn drop(&mut self) {
+ assert!(self.0.is_none());
+ }
+}
+
+impl std::fmt::Debug for ByteReader {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("ByteReader").finish()
+ }
+}
+
+pub struct LineReader(Option<LineReading>);
+
+enum LineReading {
+ Fetch(FetchReader),
+ MailboxList(MailboxListReader),
+}
+
+impl LineReader {
+ fn new(reading: LineReading) -> Self {
+ Self(Some(reading))
+ }
+
+ pub fn read_line(mut self, line: &str) -> Result<ReadResult> {
+ match self.0.take().expect("LineReader not reusable") {
+ LineReading::Fetch(reader) => reader.read_line(line),
+ LineReading::MailboxList(reader) => reader.read_line(line),
+ }
+ }
+
+ // TODO implement this for use when caller decides to LOGOUT rather than continue
+ // pub fn abort(mut self) {
+ // self.0 = None;
+ // }
+}
+
+impl Drop for LineReader {
+ fn drop(&mut self) {
+ assert!(self.0.is_none());
+ }
+}
+
+impl std::fmt::Debug for LineReader {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_tuple("LineReader")
+ .field(match &self.0 {
+ None => &"None",
+ Some(LineReading::Fetch(_)) => &"Fetch",
+ Some(LineReading::MailboxList(_)) => &"MailboxList",
+ })
+ .finish()
+ }
+}
+
+// msg-att = "(" (msg-att-dynamic / msg-att-static) *(SP (msg-att-dynamic / msg-att-static)) ")"
+struct FetchReader {
+ parens_open: u32,
+}
+
+impl FetchReader {
+ fn start(line: &str) -> Result<ReadResult> {
+ trace!("FetchReader::new({})", line);
+ if !(line.starts_with('(') || line.starts_with(')')) {
+ return Err(Error::new(format!(
+ "protocol error: msg-att doesn't start with '(' or ')': {}",
+ line
+ )));
+ }
+ Self { parens_open: 0 }.read_line(line)
+ }
+
+ fn read_line(mut self, line: &str) -> Result<ReadResult> {
+ let mut results: Vec<Fetch> = vec![];
+ let mut start = 0;
+ loop {
+ trace!("FetchReader::read_line loop looking at");
+ crate::logging::log_string_parts(log::Level::Trace, line, start, line.len());
+ let msg_att = &line[start..];
+ if msg_att.is_empty() {
+ return Err(Error::new(
+ "protocol error: FETCH response line ends without ')'",
+ ));
+ }
+ match &msg_att[..1] {
+ "(" => {
+ self.parens_open += 1;
+ start += 1;
+ }
+ ")" => {
+ trace!("FetchReader::next Done");
+ if self.parens_open > 1 {
+ return Err(Error::new(format!(
+ "protocol error: {} more parentheses opened than closed",
+ self.parens_open
+ )));
+ }
+ return Ok(ReadResult::Response(Response::ServerData(
+ ServerData::Fetch(results),
+ )));
+ }
+ " " => {
+ start += 1;
+ }
+ unexpected => {
+ return Err(Error::new(format!(
+ "protocol error: expected [ ()] not {}",
+ unexpected
+ )))
+ }
+ }
+ crate::logging::log_string_parts(log::Level::Trace, line, start, line.len());
+ // TODO why did i set this and then reset it
+ let msg_att: &str = &line[start..];
+ let mut iterator = msg_att.match_indices(&[' ', ')'][..]);
+ let (end_of_word, matched_delimiter): (usize, &str) =
+ iterator.next().ok_or_else(|| {
+ Error::new("protocol error: missing next ' ' or ')' in FETCH response")
+ })?;
+ let word: &str = &msg_att[..end_of_word];
+ trace!(
+ "FetchReader::next end_of_word={} delimiter={}: {}",
+ end_of_word,
+ matched_delimiter,
+ word
+ );
+ match word {
+ // TODO we get one of these two
+ // * 1 FETCH (FLAGS (\Seen \Deleted \Draft) UID 1)
+ // * 1 FETCH (UID 1 FLAGS (\Seen))
+ // * 1 FETCH (UID 1 BODY[] {181}
+ // TODO in response to UID STORE we get the same data twice!
+ // => 1 UID STORE 1 -FLAGS (\Seen)
+ // <= * 1 FETCH (FLAGS () UID 1)
+ // <= * 1 FETCH (UID 1 FLAGS ())
+ "UID" => {
+ if matched_delimiter == ")" {
+ return Err(Error::new("protocol error: 'UID' not followed by a uid"));
+ }
+ let (end_of_uid, _) = iterator
+ .next()
+ .ok_or_else(|| Error::new("protocol error: missing next ' ' or ')'"))?;
+ start += end_of_uid;
+ let digits = &msg_att[end_of_word + 1..end_of_uid];
+ results.push(Fetch::Uid(Uid::from(read_nz_number(digits)?)));
+ }
+ "FLAGS" => {
+ if matched_delimiter == ")" {
+ return Err(Error::new("protocol error: 'FLAGS' not followed by flags"));
+ }
+ let flags_start = end_of_word + 1;
+ let flag_start = flags_start + 1;
+ if msg_att.len() < flag_start {
+ return Err(Error::new(format!(
+ "protocol error: msg-att too short: {}",
+ msg_att
+ )));
+ }
+ if &msg_att[flags_start..flag_start] != "(" {
+ return Err(Error::new("protocol error: flags not start with '('"));
+ }
+ self.parens_open += 1;
+ let mut seen = false;
+ loop {
+ let (flag_end, matched_delimiter): (usize, &str) = iterator
+ .next()
+ .ok_or_else(|| Error::new("protocol error: FLAGS end without ')'"))?;
+ trace!(
+ "FetchReader::read_line FLAGS loop matched_delim={} looking at",
+ matched_delimiter
+ );
+ crate::logging::log_string_parts(
+ log::Level::Trace,
+ msg_att,
+ flag_start,
+ msg_att.len(),
+ );
+ let flag: &str = &msg_att[flag_start..flag_end];
+ // TODO case
+ if flag == "\\Seen" {
+ seen = true;
+ }
+ if matched_delimiter == ")" {
+ self.parens_open -= 1;
+ start += flag_end + 1;
+ results.push(Fetch::Flags(seen));
+ break;
+ }
+ }
+ }
+ "BODY[]" => {
+ // RFC3501 doesn't seem to make any promise about ordering, so we might see the UID
+ // first and then have the bytes to save, or we might have to save the bytes first
+ // and then keep reading for the UID.
+ // That would be quite painful.
+ // All implementations I've encountered so far send UID before BODY.
+ // Let's see how far we can get without it?
+ let size_spec: &str = &msg_att[end_of_word + 1..];
+ let (size, one_past_end): (usize, usize) = read_literal_size(size_spec)?;
+ if one_past_end != size_spec.len() {
+ return Err(Error::new(format!(
+ "protocol error: characters after size: {}",
+ &size_spec[one_past_end..]
+ )));
+ }
+ if self.parens_open > 1 {
+ return Err(Error::new(format!(
+ "protocol error: {} parentheses opened when BODY[] received; should be 1",
+ self.parens_open
+ )));
+ }
+ return Ok(ReadResult::FetchBytes(
+ results,
+ size,
+ LineReader::new(LineReading::Fetch(self)),
+ ));
+ }
+ "" => {
+ if matched_delimiter == ")" {
+ debug!("msg-att = ()? okay...");
+ } else {
+ todo!("what is msg-att here: {}", msg_att);
+ }
+ }
+ r => todo!("what '{:?}'", r),
+ }
+ }
+ }
+}
+
+// mailbox-list = "(" [mbx-list-flags] ")" SP (DQUOTE QUOTED-CHAR DQUOTE / nil) SP "INBOX" / astring
+struct MailboxListReader {
+ flags: String,
+ // TODO bug-prone having mutually exclusive Options but it's so internal...
+ name: Option<String>,
+ reader: Option<AStringReader>,
+}
+
+impl MailboxListReader {
+ fn start(line: &str) -> Result<ReadResult> {
+ trace!("read_mailbox_list({})", line);
+ if !line.starts_with('(') {
+ return Err(Error::new(
+ "protocol error: mailbox-list doesn't start with '('",
+ ));
+ }
+ let mbx_list_flags_close: usize = line
+ .find(')')
+ .ok_or_else(|| Error::new("protocol error: mailbox-list missing ')'"))?;
+ // mbx_list_flags_close = 6 is minimum length:
+ // 0123456
+ // ) "/" f
+ if line.len() < mbx_list_flags_close + 6 {
+ return Err(Error::new("protocol error: mailbox-list ends at ')'"));
+ }
+ // TODO handle separator. Also it might be /nil/i
+ let rest = if &line[mbx_list_flags_close + 2..mbx_list_flags_close + 3] == "\"" {
+ let (separator, rest) = unquote(&line[mbx_list_flags_close + 2..])?;
+ warn!("TODO handle mailbox separator {}", separator);
+ warn!(
+ "TODO after mailbox separator '{}'",
+ &line[rest + mbx_list_flags_close + 2..]
+ );
+ rest + mbx_list_flags_close + 2
+ } else {
+ todo!("NIL separator not yet supported");
+ };
+ if line.len() - rest < 2 {
+ return Err(Error::new(
+ "protocol error: mailbox-list missing mailbox name",
+ ));
+ }
+ if &line[rest..rest + 1] != " " {
+ return Err(Error::new(
+ "protocol error: mailbox-list missing space before mailbox name",
+ ));
+ }
+ let flags = line[1..mbx_list_flags_close].to_lowercase();
+ trace!("mbx-list-flags is '{}'", flags);
+ Ok(match AStringReader::start(&line[rest + 1..])? {
+ AStringReaderResult::NeedBytes(n, reader) => ReadResult::NeedBytes(
+ n,
+ ByteReader::new(ByteReading::MailboxList(Self {
+ flags,
+ name: None,
+ reader: Some(reader),
+ })),
+ ),
+ AStringReaderResult::Done(value, _) => {
+ trace!("mailbox is '{}'", value);
+ ReadResult::Response(Response::ServerData(ServerData::List(value, flags)))
+ }
+ })
+ }
+
+ // TODO bug-prone having both but it's so internal...
+ fn read_bytes(self, bytes: &[u8]) -> Result<ReadResult> {
+ Ok(ReadResult::NeedLine(LineReader::new(
+ LineReading::MailboxList(Self {
+ flags: self.flags,
+ name: Some(self.reader.expect("AStringReader to be here").read(bytes)?),
+ reader: None,
+ }),
+ )))
+ }
+ fn read_line(self, line: &str) -> Result<ReadResult> {
+ if !line.is_empty() {
+ return Err(Error::new(format!(
+ "protocol error: extraneous bytes after mailbox-list: {}",
+ line
+ )));
+ }
+ Ok(ReadResult::Response(Response::ServerData(
+ ServerData::List(self.name.expect("mailbox name to be here"), self.flags),
+ )))
+ }
+}
+
+fn read_untagged(line: &str) -> Result<ReadResult> {
+ trace!("read_untagged({})", line);
+ if line.is_empty() {
+ return Err(Error::new("protocol error: empty untagged response"));
+ }
+ let (word, rest) = match line.find(' ') {
+ Some(index) => (line[..index].to_ascii_uppercase(), Some(index + 1)),
+ None => (line.to_ascii_uppercase(), None),
+ };
+ match word.as_str() {
+ "OK" => {
+ // TODO NO and BAD may appear here too!
+ let index_of_resp_text = rest.ok_or_else(|| {
+ Error::new("protocol error: resp-text missing from resp-cond-state")
+ })?;
+ let (code, _) = read_resp_text(&line[index_of_resp_text..])?;
+ Ok(ReadResult::Response(Response::UntaggedStatus(Status::Ok(
+ code,
+ ))))
+ }
+ // "LIST" SP mailbox-list
+ "LIST" => rest
+ .map(|index| MailboxListReader::start(&line[index..]))
+ .ok_or_else(|| Error::new("protocol error: LIST response missing mailbox-list"))?,
+ "ESEARCH" => {
+ let messages = match rest {
+ None => &line[..0],
+ Some(rest) => &line[rest..],
+ };
+ trace!("captured messages from search: '{}'", messages);
+ Ok(ReadResult::Response(Response::ServerData(
+ ServerData::ESearch(read_esearch_response(messages)?),
+ )))
+ }
+ "SEARCH" => {
+ let messages = match rest {
+ None => &line[..0],
+ Some(rest) => &line[rest..],
+ };
+ trace!("captured messages from search: '{}'", messages);
+ Ok(ReadResult::Response(Response::ServerData(
+ ServerData::Search(read_search_response(messages)?),
+ )))
+ }
+ "CAPABILITY" => {
+ let capabilities = match rest {
+ None => &line[..0],
+ Some(rest) => &line[rest..],
+ };
+ Ok(ReadResult::Response(Response::ServerData(
+ ServerData::Capability(read_capability_data(capabilities)),
+ )))
+ }
+ "BYE" => todo!("handle BYE: {}", line),
+ _ => {
+ match rest {
+ None => {
+ trace!("ignoring {}", word);
+ Ok(ReadResult::Response(Response::ServerData(
+ ServerData::Unknown,
+ )))
+ }
+ Some(rest_index) => {
+ // We have some cases where `word` is a number and there's a second word (`word2`) we need to look at.
+ let (word2, word2_rest) = match line[rest_index..].find(' ') {
+ None => (line[rest_index..].to_ascii_uppercase(), None),
+ Some(index) => (
+ line[rest_index..rest_index + index].to_ascii_uppercase(),
+ Some(rest_index + index + 1),
+ ),
+ };
+ match word2.as_str() {
+ "EXISTS" => match word.parse::<u32>() {
+ Ok(exists) => Ok(ReadResult::Response(Response::ServerData(
+ ServerData::Exists(exists),
+ ))),
+ _ => Ok(ReadResult::Response(Response::ServerData(
+ ServerData::Unknown,
+ ))),
+ },
+ // nz-number SP "FETCH" SP msg-att
+ // nz-number is the message sequence number: we don't care.
+ "FETCH" => word2_rest
+ .map(|rest_index| FetchReader::start(&line[rest_index..]))
+ .ok_or_else(|| {
+ Error::new("protocol error: FETCH response missing msg-att")
+ })?,
+ _ => {
+ trace!("ignoring {} and {}", word, word2);
+ Ok(ReadResult::Response(Response::ServerData(
+ ServerData::Unknown,
+ )))
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+fn unquote(quoted: &str) -> Result<(String, usize)> {
+ let mut result = String::new();
+ let mut escaped = false;
+ // Assume first byte is " and skip that.
+ for (i, c) in quoted[1..].char_indices() {
+ if escaped {
+ escaped = false;
+ result.push(c);
+ } else if c == '\\' {
+ escaped = true;
+ } else if c == '"' {
+ return Ok((result, i + 2));
+ } else {
+ result.push(c);
+ }
+ }
+ Err(Error::new("protocol error: str missing close quote"))
+}
+
+// resp-text = ["[" resp-text-code "]" SP] text
+fn read_resp_text(resp_text: &str) -> Result<(ResponseCode, &str)> {
+ if resp_text.is_empty() {
+ return Err(Error::new("protocol error: resp-text is empty"));
+ }
+ if &resp_text[0..1] != "[" {
+ return Ok((ResponseCode::Unknown, resp_text));
+ }
+ let close_bracket: usize = resp_text
+ .find(']')
+ .ok_or_else(|| Error::new("protocol error: no close bracket in resp-text"))?;
+ let text = if close_bracket + 1 == resp_text.len() {
+ debug!("GMAIL QUIRK resp-text: {}", resp_text);
+ // GMail responds with
+ // * OK [HIGHESTMODSEQ 8869]
+ // RFC 3501 defines it as
+ // resp-text = ["[" resp-text-code "]" SP] text
+ // text = 1*TEXT-CHAR
+ // The text is not optional, and if the optional bracketed text MUST be followed by a space.
+ &resp_text[..0]
+ } else {
+ if resp_text.len() < close_bracket + 2 {
+ return Err(Error::new(format!(
+ "protocol error: resp-text too short: {}",
+ resp_text
+ )));
+ }
+ if &resp_text[close_bracket + 1..close_bracket + 2] != " " {
+ return Err(Error::new(
+ "protocol error: after resp-text-code is something not a space",
+ ));
+ }
+ &resp_text[close_bracket + 2..]
+ };
+ let (status, resp_text_code) = match resp_text[1..close_bracket].find(' ') {
+ Some(index) => {
+ if index + 2 >= close_bracket {
+ return Err(Error::new(format!(
+ "protocol error: resp-text too short: {}",
+ resp_text
+ )));
+ }
+ (
+ resp_text[1..index + 1].to_ascii_uppercase(),
+ &resp_text[index + 2..close_bracket],
+ )
+ }
+ None => (
+ resp_text[1..close_bracket].to_ascii_uppercase(),
+ &resp_text[..1],
+ ),
+ };
+ Ok(match status.as_str() {
+ // resp-code-apnd = "APPENDUID" SP nz-number SP append-uid
+ "APPENDUID" => (read_appenduid(resp_text_code)?, text),
+ "CAPABILITY" => (
+ ResponseCode::Capability(read_capability_data(resp_text_code)),
+ text,
+ ),
+ // resp-code-copy = "COPYUID" SP nz-number SP uid-set SP uid-set
+ "COPYUID" => (read_copyuid(resp_text_code)?, text),
+ // "UIDNEXT" SP nz-number
+ "UIDNEXT" => {
+ let result = (
+ ResponseCode::UidNext(Uid::from(read_nz_number(resp_text_code)?)),
+ text,
+ );
+ trace!("{:?}", result);
+ result
+ }
+ "UIDVALIDITY" => (
+ ResponseCode::UidValidity(UidValidity::from(read_nz_number(resp_text_code)?)),
+ text,
+ ),
+ _ => {
+ trace!("Unknown response code {} {}", status, text);
+ (ResponseCode::Unknown, text)
+ }
+ })
+}
+
+fn read_resp_cond_state(resp_cond_state: &str) -> Result<Status> {
+ let index: usize = resp_cond_state.find(' ').ok_or_else(|| {
+ Error::new(format!(
+ "protocol error: can't find ' ' in resp-cond-state: {}",
+ resp_cond_state
+ ))
+ })?;
+ if index + 1 >= resp_cond_state.len() {
+ return Err(Error::new(format!(
+ "protocol error: resp-cond-state too short: {}",
+ resp_cond_state
+ )));
+ }
+ match &resp_cond_state[..index] {
+ "OK" => {
+ let (code, _) = read_resp_text(&resp_cond_state[index + 1..])?;
+ Ok(Status::Ok(code))
+ }
+ "NO" => {
+ let (code, text) = read_resp_text(&resp_cond_state[index + 1..])?;
+ Ok(Status::No(code, text.to_owned()))
+ }
+ "BAD" => {
+ let (code, text) = read_resp_text(&resp_cond_state[index + 1..])?;
+ Ok(Status::Bad(code, text.to_owned()))
+ }
+ trash => Err(Error::new(format!(
+ "protocol error: resp-cond-state must start with (OK | NO | BAD) not {}",
+ trash
+ ))),
+ }
+}
+
+// atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards /
+// quoted-specials / resp-specials
+// list-wildcards = "%" / "*"
+// quoted-specials = DQUOTE / "\"
+// resp-specials = "]"
+const ATOM_SPECIALS: &[char] = &['(', ')', '{', ' ', '%', '*', '"', '\\', ']'];
+
+// Read one 'astring', reading additional bytes from the server if it's a 'literal'.
+// Returns (slice of the fully-read astring, slice of the astring parameter not processed herein).
+// The full specification of 'astring' follows, but all we need care about it:
+// - non-double-quoted-and-no-whitespace-word
+// - "double-quoted text with backslash-escaping"
+// - {N}\r\n where N is non-zero unsigned 32-bit number of bytes to read
+// astring = 1*ASTRING-CHAR / string
+// ASTRING-CHAR = ATOM-CHAR / resp-specials
+// ATOM-CHAR = <any CHAR except atom-specials>
+// atom-specials = "(" / ")" / "{" / SP / CTL / list-wildcards / quoted-specials / resp-specials
+// list-wildcards = "%" / "*"
+// literal = "{" number "}" CRLF *CHAR8
+// ; Number represents the number of CHAR8s
+// number = 1*DIGIT
+// ; Unsigned 32-bit integer
+// quoted = DQUOTE *QUOTED-CHAR DQUOTE
+// QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials
+// quoted-specials = DQUOTE / "\"
+// resp-specials = "]"
+// string = quoted / literal
+enum AStringReaderResult {
+ NeedBytes(usize, AStringReader),
+ Done(String, usize),
+}
+struct AStringReader(usize);
+
+impl AStringReader {
+ pub fn start(line: &str) -> Result<AStringReaderResult> {
+ if line.is_empty() {
+ return Err(Error::new("protocol error: empty astring"));
+ }
+ Ok(match &line[..1] {
+ "{" => {
+ let (n, one_past_end) = read_literal_size(line)?;
+ if one_past_end != line.len() {
+ return Err(Error::new(format!(
+ "protocol error: extraneous bytes after {{N}}: {}",
+ &line[one_past_end..]
+ )));
+ }
+ AStringReaderResult::NeedBytes(n, Self(n))
+ }
+ "\"" => {
+ let (value, rest_index) = unquote(line)?;
+ AStringReaderResult::Done(value, rest_index)
+ }
+ _ => {
+ let end_of_atom = line.find(ATOM_SPECIALS).unwrap_or(line.len());
+ // TODO end_of_atom not tested! Only use of AStringReader is MailboxListReader,
+ // whose astring comes at the end of the line so it doesn't care.
+ AStringReaderResult::Done(line[..end_of_atom].to_string(), end_of_atom)
+ }
+ })
+ }
+
+ pub fn read(self, bytes: &[u8]) -> Result<String> {
+ if bytes.len() != self.0 {
+ Err(Error::new(format!(
+ "INTERNAL ERROR: AStringReader expected {} bytes, got {}",
+ self.0,
+ bytes.len()
+ )))
+ } else {
+ Ok(std::str::from_utf8(bytes)
+ .map_err(|e| {
+ Error::describe(
+ e,
+ format_args!("protocol error: AStringReader requires UTF-8"),
+ )
+ })?
+ .to_owned())
+ }
+ }
+}
+
+/// Returns (the literal size, one past the close brace).
+fn read_literal_size(literal: &str) -> Result<(usize, usize)> {
+ let end: usize = literal.find('}').ok_or_else(|| {
+ Error::new(format!(
+ "protocol error: close brace not found in literal {}",
+ literal
+ ))
+ })?;
+ let size_str = &literal[1..end];
+ trace!("read_literal_size({}[1..{}]) -> {}", literal, end, size_str);
+ Ok((
+ size_str.parse().map_err(|e| {
+ Error::describe(
+ e,
+ format_args!("protocol error decoding literal size {}", size_str),
+ )
+ })?,
+ end + 1,
+ ))
+}
+
+// nz-number SP append-uid
+fn read_appenduid(appenduid: &str) -> Result<ResponseCode> {
+ match appenduid.split(' ').collect::<Vec<_>>().as_slice() {
+ [uidvalidity, uid] => Ok(ResponseCode::AppendUid(
+ UidValidity::from(read_nz_number(uidvalidity)?),
+ Uid::from(read_nz_number(uid)?),
+ )),
+ _ => Err(Error::new(format!(
+ "protocol error: APPENDUID response too small: {}",
+ appenduid
+ ))),
+ }
+}
+
+// nz-number SP uid-set SP uid-set
+fn read_copyuid(copyuid: &str) -> Result<ResponseCode> {
+ match copyuid.split(' ').collect::<Vec<_>>().as_slice() {
+ [uid_validity, _, uid] => Ok(ResponseCode::CopyUid(
+ UidValidity::from(read_nz_number(uid_validity)?),
+ Uid::from(read_nz_number(uid)?),
+ )),
+ _ => Err(Error::new(format!(
+ "protocol error: COPYUID response too small: {}",
+ copyuid
+ ))),
+ }
+}
+
+// nz-number = digit-nz *DIGIT
+// ; Non-zero unsigned 32-bit integer
+// ; (0 < n < 4,294,967,296)
+fn read_nz_number(s: &str) -> Result<std::num::NonZeroU32> {
+ s.parse()
+ .map_err(|e| Error::describe(e, format_args!("protocol error: nz-number")))
+}
+
+fn read_capability_data(s: &str) -> Capabilities {
+ let result = Capabilities::from(
+ nmh::hacks::split(s)
+ .map(read_capability)
+ .collect::<std::collections::BTreeSet<_>>(),
+ );
+ debug!("read {:?} from {}", result, s);
+ result
+}
+
+fn read_capability(s: &str) -> Capability {
+ let upper: String = s.to_ascii_uppercase();
+ match upper.split('=').collect::<Vec<_>>().as_slice() {
+ ["AUTH", "PLAIN"] => return Capability::Auth(nmh::netsec::sasl::Mechanism::Plain),
+ ["AUTH", "XOAUTH2"] => return Capability::Auth(nmh::netsec::sasl::Mechanism::XOAuth2),
+ _ => {}
+ }
+ match upper.as_str() {
+ "ESEARCH" => Capability::ESearch,
+ "IMAP4REV1" => Capability::IMAP4Rev1,
+ "SASL-IR" => Capability::SASLIR,
+ "STARTTLS" => Capability::StartTLS,
+ "UIDPLUS" => Capability::UIDPlus,
+ _ => Capability::Unknown(upper),
+ }
+}
+
+// mailbox-data = "SEARCH" *(SP nz-number)
+// <= * SEARCH 2 3 4 5 7 8 9 10 11
+fn read_search_response(s: &str) -> Result<uid::Set> {
+ let mut uids = uid::Set::new();
+ for uid_result in nmh::hacks::split(s).map(read_nz_number) {
+ uids.insert(uid_result?.into());
+ }
+ Ok(uids)
+}
+
+// esearch-response = "ESEARCH" [search-correlator] [SP "UID"] *(SP search-return-data)
+// <= * ESEARCH (TAG "A283") UID ALL 2,10:11
+fn read_esearch_response(s: &str) -> Result<uid::Set> {
+ let uid_indicator = discard_search_correlator(s)?;
+ let return_data = discard_search_uid(uid_indicator)?;
+ if return_data.is_empty() {
+ // When no messages are returned, Dovecot legally omits search-return-data; Panda does not.
+ Ok(uid::Set::new())
+ } else {
+ let sequence_set = discard_search_all(return_data)?;
+ read_sequence_set(sequence_set)
+ }
+}
+
+// TODO multi-line literals are allowed or tag-string. We don't use tags that
+// would REQUIRE multi-line literals, but nevertheless it is perfectly legal
+// for a server to return them to us that way.
+// search-correlator = SP "(" "TAG" SP tag-string ")"
+// tag-string = string
+fn discard_search_correlator(s: &str) -> Result<&str> {
+ // TODO this was supposed to start with a space.
+ match s.find('(') {
+ None => Ok(s),
+ Some(open) => match s.find(')') {
+ None => todo!(
+ "protocol error: search-correlator open paren without close paren: {}",
+ s
+ ),
+ Some(close) => Ok(&s[open + close + 1..]),
+ },
+ }
+}
+
+fn discard_search_uid(s: &str) -> Result<&str> {
+ // RFC 4731 3.1. New SEARCH/UID SEARCH Result Options says this is required since we UID ESEARCH:
+ // "An extended UID SEARCH command MUST cause an ESEARCH response with the UID indicator present."
+ if !s.starts_with(" UID") {
+ todo!("protocol error: search-response missing UID indicator")
+ }
+ Ok(&s[4..])
+}
+
+// search-return-data = search-modifier-name SP search-return-value
+// search-return-data = "ALL" SP sequence-set
+fn discard_search_all(s: &str) -> Result<&str> {
+ if !s.starts_with(" ALL ") {
+ todo!("protocol error: search-modifier-name ALL required")
+ }
+ Ok(&s[5..])
+}
+
+// sequence-set = (seq-number / seq-range) *("," sequence-set)
+fn read_sequence_set(s: &str) -> Result<uid::Set> {
+ let mut uids = uid::Set::new();
+ for seq_set in s.split(',') {
+ match seq_set.split(':').collect::<Vec<_>>().as_slice() {
+ [uid] => {
+ uids.insert(read_nz_number(uid)?.into());
+ }
+ [beg, end] => {
+ let beg_u32: u32 = read_nz_number(beg)?.into();
+ let end_u32: u32 = read_nz_number(end)?.into();
+ for uid in beg_u32..end_u32 + 1 {
+ uids.insert(uid);
+ }
+ }
+ _ => {
+ todo!("protocol error: > 2 colons in {}", seq_set);
+ }
+ }
+ }
+ Ok(uids)
+}
+
+#[cfg(test)]
+mod tests;
--- /dev/null
+//! Models of IMAP response data used by the high-level [crate::imap::Session].
+
+use std::collections::BTreeSet;
+
+use nmhimaptypes::{uid, Uid, UidValidity};
+
+/// A server greeting returned by [super::read_greeting].
+#[derive(Debug)]
+pub enum Greeting {
+ Ok(ResponseCode),
+ PreAuth(ResponseCode),
+}
+
+/// RFC 3501 7. Server Responses says server responses are in three forms:
+/// - status responses
+/// - server data
+/// - command continuation request
+/// Status responses can be tagged or untagged.
+/// Tagged status responses indicate the completion result (OK, NO, or BAD
+/// status) of a client command, and have a tag matching the command.
+#[derive(Debug)]
+pub enum Response {
+ ServerData(ServerData),
+ UntaggedStatus(Status /*, parsed response bits */),
+ // (tag, Status)
+ TaggedStatus(String, Status),
+ //Bye(&str),
+}
+
+// Server data are untagged.
+pub enum ServerData {
+ Unknown,
+ /// the server's current capabilities, which may vary from state to state
+ Capability(Capabilities),
+ /// the number of messages existing in the mailbox the response pertains to
+ Exists(u32),
+ Fetch(Vec<Fetch>),
+ /// (mailbox name, flags)
+ List(String, String),
+ ESearch(uid::Set),
+ Search(uid::Set),
+}
+
+impl std::fmt::Debug for ServerData {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ ServerData::Unknown => f.debug_tuple("Unknown").finish(),
+ ServerData::Capability(cap) => f.debug_tuple("Capability").field(cap).finish(),
+ ServerData::Exists(n) => f.debug_tuple("ServerData::Exists").field(n).finish(),
+ ServerData::Fetch(_) => f.debug_tuple("ServerData::Fetch").finish(),
+ ServerData::List(mailbox_name, flags) => f
+ .debug_tuple("List")
+ .field(mailbox_name)
+ .field(flags)
+ .finish(),
+ ServerData::ESearch(set) => f.debug_tuple("ESearch").field(set).finish(),
+ ServerData::Search(set) => f.debug_tuple("Search").field(set).finish(),
+ }
+ }
+}
+
+/// A capability offered by a server.
+#[derive(Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
+pub enum Capability {
+ /// RFC 3501 - IMAP4rev1
+ Auth(nmh::netsec::sasl::Mechanism),
+ /// RFC 4731 - SEARCH results in sequence-set syntax
+ ESearch,
+ /// RFC 3501 - IMAP4rev1
+ IMAP4Rev1,
+ /// RFC 3501 - IMAP4rev1
+ StartTLS,
+ /// RFC 4959 - SASL Initial Client Response
+ SASLIR,
+ /// RFC 4315 - UIDPLUS
+ UIDPlus,
+ Unknown(String),
+}
+
+/// Part of a [crate::imap::FetchResult] operation.
+#[derive(Debug)]
+pub enum Fetch {
+ /// Whether this message has the `\Seen` flag (only flag we care about right now).
+ Flags(bool),
+ Uid(Uid),
+}
+
+#[derive(Debug)]
+pub enum Status {
+ Ok(ResponseCode),
+ No(ResponseCode, String),
+ Bad(ResponseCode, String),
+}
+
+// TODO
+// Status responses we care about:
+// [ALERT text...] - display 'text...' to user
+// [BADCHARSET ...] - internal error; crash and log
+// [PARSE text...] - display 'text...' and skip this message (TODO can we test this?)
+// [PERMANENTFLAGS (... \* ...)] - look for '\*' to for keywords
+// [READ-ONLY]
+// [READ-WRITE]
+#[derive(Debug)]
+pub enum ResponseCode {
+ /// used for both unknown response code and no response code at all because we don't need to distinguish those cases.
+ Unknown,
+ AppendUid(UidValidity, Uid),
+ Capability(Capabilities),
+ CopyUid(UidValidity, Uid),
+ UidNext(Uid),
+ UidValidity(UidValidity),
+}
+
+/// Set of [Capability]s a server offers.
+pub struct Capabilities(BTreeSet<Capability>);
+
+impl Capabilities {
+ /// Return true if `capability` is in this set.
+ pub fn has(&self, capability: Capability) -> bool {
+ self.0.contains(&capability)
+ }
+
+ pub fn iter(&self) -> std::collections::btree_set::Iter<Capability> {
+ self.0.iter()
+ }
+}
+
+impl From<BTreeSet<Capability>> for Capabilities {
+ fn from(set: BTreeSet<Capability>) -> Self {
+ Self(set)
+ }
+}
+
+impl std::fmt::Debug for Capabilities {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_list()
+ .entries(self.0.iter().filter(|c| {
+ // Only including known capabilities for less verbose debug output.
+ !matches!(c, Capability::Unknown(_))
+ }))
+ .finish()
+ }
+}
--- /dev/null
+use std::num::NonZeroU32;
+
+use super::*;
+
+mod capability {
+ use super::*;
+
+ mod greeting {
+ use super::*;
+
+ fn read_greeting_as_debug_string(s: &str) -> String {
+ let mut owned = s.to_owned();
+ format!("{:?}", read_greeting(&mut owned).unwrap())
+ }
+
+ #[test]
+ fn panda_preauth() {
+ assert_eq!(
+ "PreAuth(Capability([IMAP4Rev1, UIDPlus]))",
+ read_greeting_as_debug_string("* PREAUTH [CAPABILITY IMAP4REV1 I18NLEVEL=1 LITERAL+ IDLE UIDPLUS NAMESPACE CHILDREN MAILBOX-REFERRALS BINARY UNSELECT WITHIN SORT THREAD=REFERENCES THREAD=ORDEREDSUBJECT MULTIAPPEND SCAN] Pre-authenticated")
+ )
+ }
+
+ #[test]
+ fn dovecot() {
+ assert_eq!(
+ "Ok(Capability([Auth(Plain), IMAP4Rev1, StartTLS, SASLIR]))",
+ read_greeting_as_debug_string("* OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+ STARTTLS AUTH=PLAIN] Dovecot ready.")
+ )
+ }
+
+ #[test]
+ fn cyrus() {
+ assert_eq!(
+ "Ok(Capability([Auth(Plain), IMAP4Rev1, SASLIR]))",
+ read_greeting_as_debug_string("* OK [CAPABILITY IMAP4rev1 LITERAL+ ID ENABLE AUTH=SCRAM-SHA-1 AUTH=SCRAM-SHA-256 AUTH=DIGEST-MD5 AUTH=CRAM-MD5 AUTH=NTLM AUTH=PLAIN AUTH=LOGIN SASL-IR] Cyrus IMAP 3.2.3 server ready")
+ )
+ }
+
+ #[test]
+ fn gandi() {
+ assert_eq!(
+ "Ok(Capability([Auth(Plain), IMAP4Rev1, StartTLS, SASLIR]))",
+ read_greeting_as_debug_string("* OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE LITERAL+ STARTTLS AUTH=PLAIN] Dovecot ready.")
+ )
+ }
+
+ #[test]
+ fn gmail_omits_capabilities() {
+ assert_eq!(
+ "Ok(Unknown)",
+ read_greeting_as_debug_string(
+ "* OK Gimap ready for requests from 70.237.130.101 z4mb18213485oce"
+ )
+ )
+ }
+ }
+
+ mod auth_response {
+ use super::*;
+
+ fn read_response_as_debug_string(s: &str) -> String {
+ match read_response(s).unwrap() {
+ ReadResult::Response(response) => format!("{:?}", response),
+ ReadResult::CommandContinuationRequest => {
+ panic!("don't want CommandContinuationRequest")
+ }
+ ReadResult::FetchBytes(_, _, _) => panic!("don't want FetchBytes"),
+ ReadResult::NeedBytes(_, _) => panic!("don't want NeedBytes"),
+ ReadResult::NeedLine(_) => panic!("don't want NeedLine"),
+ }
+ }
+
+ #[test]
+ fn authenticate_dovecot() {
+ assert_eq!(
+ "TaggedStatus(\"1\", Ok(Capability([ESearch, IMAP4Rev1, SASLIR, UIDPlus])))",
+ read_response_as_debug_string("1 OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SNIPPET=FUZZY PREVIEW=FUZZY STATUS=SIZE SAVEDATE LITERAL+ NOTIFY] Logged in")
+ )
+ }
+
+ #[test]
+ fn authenticate_cyrus() {
+ assert_eq!(
+ "TaggedStatus(\"1\", Ok(Capability([ESearch, IMAP4Rev1, UIDPlus])))",
+ read_response_as_debug_string("1 OK [CAPABILITY IMAP4rev1 LITERAL+ ID ENABLE ACL RIGHTS=kxten QUOTA MAILBOX-REFERRALS NAMESPACE UIDPLUS NO_ATOMIC_RENAME UNSELECT CHILDREN MULTIAPPEND BINARY CATENATE CONDSTORE ESEARCH SEARCH=FUZZY SORT SORT=MODSEQ SORT=DISPLAY SORT=UID THREAD=ORDEREDSUBJECT THREAD=REFERENCES THREAD=REFS ANNOTATE-EXPERIMENT-1 METADATA LIST-EXTENDED LIST-STATUS LIST-MYRIGHTS LIST-METADATA WITHIN QRESYNC SCAN XLIST XMOVE MOVE SPECIAL-USE CREATE-SPECIAL-USE DIGEST=SHA1 X-REPLICATION STATUS=SIZE OBJECTID SAVEDATE X-CREATEDMODSEQ PREVIEW=FUZZY URLAUTH URLAUTH=BINARY LOGINDISABLED COMPRESS=DEFLATE X-QUOTA=STORAGE X-QUOTA=MESSAGE X-QUOTA=X-ANNOTATION-STORAGE X-QUOTA=X-NUM-FOLDERS IDLE] Success (no protection)")
+ )
+ }
+
+ #[test]
+ fn authenticate_gandi() {
+ assert_eq!(
+ "TaggedStatus(\"1\", Ok(Capability([ESearch, IMAP4Rev1, SASLIR, UIDPlus])))",
+ read_response_as_debug_string("1 OK [CAPABILITY IMAP4rev1 SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH LIST-STATUS BINARY MOVE SNIPPET=FUZZY PREVIEW=FUZZY PREVIEW STATUS=SIZE SAVEDATE LITERAL+ NOTIFY QUOTA] Logged in")
+ )
+ }
+
+ #[test]
+ fn capability_unauthenticated_gmail() {
+ assert_eq!(
+ "ServerData(Capability([Auth(Plain), Auth(XOAuth2), IMAP4Rev1, SASLIR]))",
+ read_response_as_debug_string("* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH2 AUTH=PLAIN AUTH=PLAIN-CLIENTTOKEN AUTH=OAUTHBEARER AUTH=XOAUTH")
+ )
+ }
+
+ #[test]
+ fn capability_authenticated_gmail() {
+ assert_eq!(
+ "ServerData(Capability([ESearch, IMAP4Rev1, UIDPlus]))",
+ read_response_as_debug_string("* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- SPECIAL-USE APPENDLIMIT=35651584")
+ )
+ }
+ }
+}
+
+mod unquote {
+ use super::*;
+
+ #[test]
+ fn simple() {
+ let quoted = r#""simple" rest"#;
+ let (unquoted, rest) = unquote(quoted).unwrap();
+ assert_eq!((unquoted, "ed[rest..]), ("simple".to_string(), " rest"));
+ }
+
+ #[test]
+ fn less_simple() {
+ let quoted = r#""not \\ \"so simple" rest"#;
+ let (unquoted, rest) = unquote(quoted).unwrap();
+ assert_eq!(
+ (unquoted, "ed[rest..]),
+ (r#"not \ "so simple"#.to_string(), " rest")
+ );
+ }
+
+ #[test]
+ fn starts_with_wrong_character() {
+ // TODO This shouldn't work but this is an internal function and we only call it when we detect a double-quote so...
+ let quoted = " oops\"";
+ let (unquoted, rest) = unquote(quoted).unwrap();
+ assert_eq!((unquoted, "ed[rest..]), ("oops".to_string(), ""));
+ }
+
+ #[test]
+ #[should_panic(expected = "byte index 1 is out of bounds")]
+ fn empty() {
+ unquote("").unwrap();
+ }
+
+ #[test]
+ fn missing_closing_quote() {
+ assert_eq!(
+ unquote("\"foo").unwrap_err().to_string(),
+ "protocol error: str missing close quote"
+ );
+ }
+}
+
+mod read_resp_text {
+ use super::*;
+
+ #[test]
+ fn copyuid() {
+ let (code, text) = read_resp_text("[COPYUID 1613513882 3 2] UID COPY completed").unwrap();
+ assert_eq!(
+ code,
+ ResponseCode::CopyUid(
+ UidValidity::from(NonZeroU32::new(1613513882).unwrap()),
+ Uid::from(NonZeroU32::new(2).unwrap())
+ )
+ );
+ assert_eq!(text, "UID COPY completed");
+ }
+
+ // TODO should this be ProtocolError?
+ #[test]
+ fn missing_leading_bracket() {
+ let (code, text) = read_resp_text("COPYUID 1613513882 3 2] UID COPY completed").unwrap();
+ assert_eq!(code, ResponseCode::Unknown);
+ assert_eq!(text, "COPYUID 1613513882 3 2] UID COPY completed");
+ }
+}
+
+impl std::cmp::PartialEq for ResponseCode {
+ fn eq(&self, other: &Self) -> bool {
+ if std::mem::discriminant(self) != std::mem::discriminant(other) {
+ return false;
+ }
+ match self {
+ Self::Unknown => true,
+ Self::CopyUid(uv, uid) => {
+ if let Self::CopyUid(other_uv, other_uid) = other {
+ uv == other_uv && uid == other_uid
+ } else {
+ unreachable!("already checked discriminant")
+ }
+ }
+ _ => todo!("minimal test implementation"),
+ }
+ }
+}
--- /dev/null
+//! See [crate::imap] for overview.
+
+use log::trace;
+
+use nmh::Netsec;
+use nmhimaptypes::{Uid, UidValidity};
+
+use crate::imap;
+use imap::protocol::{
+ read_response, ByteReader, Capabilities, Capability, ReadResult, Response, ResponseCode,
+ ServerData, Status,
+};
+use imap::{Flag, Mailbox};
+
+mod fetch;
+pub mod mailbox;
+
+pub use fetch::{FetchResult, MessageReader, MessageResult};
+
+pub type Error = crate::error::RecoverableError<Session, nmh::Error>;
+
+/// Manages the "authenticated" state of an IMAP session.
+/// See [crate::imap] for overview.
+/// Some errors are recoverable; for other errors, this sends a "BYE" command to the server and
+/// closes the connection without reading a response.
+/// To close the connection, simply [Drop] this.
+pub struct Session {
+ netsec: Netsec,
+ buf: Vec<u8>,
+ esearch: bool,
+}
+
+impl Session {
+ /// Exists for [crate::imap::auth::login]; use that.
+ pub fn new(netsec: Netsec, capabilities: Capabilities) -> Result<Self, nmh::Error> {
+ if !capabilities.has(Capability::UIDPlus) {
+ Err(nmh::Error::new(
+ "server doesn't offer required capability UIDPLUS",
+ ))
+ } else {
+ Ok(Self {
+ netsec,
+ buf: vec![0; 8192],
+ esearch: capabilities.has(Capability::ESearch),
+ })
+ }
+ }
+
+ /// Delete the named `mailbox`.
+ pub fn delete(mut self, mailbox: &imap::mailbox::Name) -> Result<Self, Error> {
+ // TODO derive tag from request-in-flight counter? We don't need these tags, and it wasn't
+ // clear to my eyes whether it's actually legal to reuse a tag like this. So far I've
+ // gotten away with it, but best not to make assumptions.
+ self.write_and_flush(&format!("1 DELETE {}\r\n", mailbox.as_str()))?;
+ loop {
+ match self.read_response()? {
+ Response::TaggedStatus(_, Status::Ok(_)) => {
+ break;
+ }
+ Response::TaggedStatus(_, _) => todo!("non-OK status"),
+ ignored => {
+ trace!("ignoring {:?}", ignored);
+ }
+ }
+ }
+ Ok(self)
+ }
+
+ /// Append a new message to the named `mailbox`, with specified `contents` and `flags`.
+ /// Return the message's UID and the mailbox's UIDVALIDITY, which may have changed.
+ pub fn append(
+ mut self,
+ mailbox: &imap::mailbox::Name,
+ flags: &[Flag],
+ contents: &str,
+ ) -> Result<((UidValidity, Uid), Self), Error> {
+ let flags_str = if flags.is_empty() {
+ String::new()
+ } else {
+ format!(" ({})", to_string_join(flags, " "))
+ };
+ self.write_and_flush(&format!(
+ "1 APPEND {}{} {{{}}}\r\n",
+ mailbox,
+ flags_str,
+ contents.len()
+ ))?;
+ match read_response(self.read_line()?)? {
+ ReadResult::CommandContinuationRequest => {}
+ // It's not clear from RFC3501, but I don't think anything else can come before the continuation request.
+ other => todo!("expecting continue-req not {:?}", other),
+ }
+ self.write(contents)?;
+ self.write_and_flush("\r\n")?;
+ loop {
+ match self.read_response()? {
+ // TODO Bail out if UIDVALIDITY changed; tell user to fix with inc.
+ Response::TaggedStatus(
+ _,
+ Status::Ok(ResponseCode::AppendUid(uidvalidity, uid)),
+ ) => {
+ break Ok(((uidvalidity, uid), self));
+ }
+ Response::TaggedStatus(_, Status::Ok(_)) => {
+ todo!("APPENDUID response too small");
+ }
+ ignored => {
+ trace!("ignoring {:?}", ignored);
+ }
+ }
+ }
+ }
+
+ /// Create the named `mailbox`.
+ pub fn create(mut self, mailbox: &str) -> Result<Self, Error> {
+ self.write_and_flush(&format!("1 CREATE \"{}\"\r\n", mailbox))?;
+ let status: Status = loop {
+ match self.read_response()? {
+ Response::TaggedStatus(_tag, status) => break status,
+ ignored => {
+ trace!("ignoring {:?}", ignored);
+ }
+ }
+ };
+ match status {
+ Status::No(_, message) => Err(Error::recoverable(nmh::Error::new(message), self)),
+ // TODO does BAD always mean our bug or can it be user error?
+ Status::Bad(code, message) => todo!("BAD {:?} {}", code, message),
+ Status::Ok(_) => Ok(self),
+ }
+ }
+
+ /// Select the named `mailbox`, entering the Selected state. See [crate::imap] for overview.
+ pub fn select(mut self, mailbox: &imap::mailbox::Name) -> Result<mailbox::Selected, Error> {
+ self.write_and_flush(&format!("1 SELECT {}\r\n", quote(mailbox.as_str())))?;
+ let mut message_count = 0;
+ let mut uid_next: Option<Uid> = None;
+ let mut uid_validity: Option<UidValidity> = None;
+ loop {
+ match self.read_response()? {
+ Response::TaggedStatus(_, Status::No(_, message)) => {
+ return Err(Error::recoverable(nmh::Error::new(message), self));
+ }
+ Response::ServerData(ServerData::Exists(value)) => {
+ message_count = value;
+ }
+ Response::UntaggedStatus(Status::Ok(ResponseCode::UidNext(value))) => {
+ uid_next = Some(value);
+ }
+ Response::UntaggedStatus(Status::Ok(ResponseCode::UidValidity(value))) => {
+ uid_validity = Some(value);
+ }
+ Response::TaggedStatus(_, Status::Ok(_)) => {
+ let uid_next = uid_next.ok_or_else(|| {
+ Error::unrecoverable(nmh::Error::new(
+ "expected UIDNEXT missing from response",
+ ))
+ })?;
+ let uid_validity = uid_validity.ok_or_else(|| {
+ Error::unrecoverable(nmh::Error::new(
+ "expected UIDVALIDITY missing from response",
+ ))
+ })?;
+ return Ok(mailbox::Selected {
+ message_count,
+ uid_next,
+ uid_validity,
+ mailbox_session: mailbox::Session::new(
+ {
+ let session_uidnext: u32 = uid_next.into();
+ session_uidnext - 1
+ },
+ self,
+ ),
+ });
+ }
+ ignored => {
+ trace!("ignoring {:?}", ignored);
+ }
+ }
+ }
+ }
+
+ /// Return the list of mailboxes under `mailbox_root`, which may be empty.
+ /// Returned in server order, which may or may not be sorted.
+ pub fn list(mut self, mailbox_root: &str) -> Result<(Vec<Mailbox>, Self), Error> {
+ self.write_and_flush(&format!("1 LIST \"\" {}*\r\n", mailbox_root))?;
+ let mut mailboxes = Vec::<Mailbox>::new();
+ loop {
+ match self.read_response()? {
+ Response::TaggedStatus(_, Status::No(code, message)) => {
+ log::error!("NO {:?} {}", code, message);
+ return Err(Error::recoverable(
+ // TODO pretty this up; can `code` be interpreted?
+ nmh::Error::new(format!("NO response from LIST: {:?} {}", code, message)),
+ self,
+ ));
+ }
+ Response::TaggedStatus(_, Status::Bad(code, message)) => {
+ // TODO does BAD always mean our bug or can it be user error?
+ return Err(Error::unrecoverable(nmh::Error::new(format!(
+ "BAD response from LIST: {:?} {}",
+ code, message
+ ))));
+ }
+ Response::TaggedStatus(_, Status::Ok(_)) => {
+ break;
+ }
+ Response::ServerData(ServerData::List(mailbox_name, flags)) => {
+ mailboxes.push(Mailbox {
+ name: imap::mailbox::Name::from(mailbox_name),
+ selectable: !flags.contains("\\noselect"),
+ });
+ }
+ ignored => {
+ trace!("ignoring {:?}", ignored);
+ }
+ }
+ }
+ Ok((mailboxes, self))
+ }
+
+ fn close(mut self) -> Result<Self, nmh::Error> {
+ self.write_and_flush("close CLOSE\r\n")?;
+ loop {
+ match self.read_response()? {
+ Response::TaggedStatus(_, Status::Ok(_)) => {
+ break;
+ }
+ Response::TaggedStatus(_, status) => {
+ todo!("only handling OK from CLOSE not {:?}", status)
+ }
+ ignored => {
+ trace!("ignoring {:?}", ignored);
+ }
+ }
+ }
+ Ok(self)
+ }
+
+ fn feed_bytes(&mut self, n: usize, reader: ByteReader) -> Result<ReadResult, nmh::Error> {
+ if n > self.buf.len() {
+ // We have 8K buffer and this is used not for reading messages (which may be large) but for
+ // reading other literals such as mailbox names (which should be small). If there's some
+ // reason to support reading >8K here, imap::protocol::ByteReader can be made to loop.
+ todo!("cowardly refusing to read {} bytes", n);
+ }
+ let mut bytes_read = 0;
+ while bytes_read < n {
+ bytes_read += self
+ .netsec
+ .read(&mut self.buf[bytes_read..n - bytes_read])?;
+ }
+ reader.read_bytes(&self.buf[..n])
+ }
+
+ fn read_response(&mut self) -> Result<Response, nmh::Error> {
+ let mut result: ReadResult = read_response(self.read_line()?)?;
+ loop {
+ match result {
+ ReadResult::CommandContinuationRequest => {
+ return Err(nmh::Error::new("unexpected continue-req"))
+ }
+ ReadResult::FetchBytes(_, _, _) => {
+ return Err(nmh::Error::new("unexpected FETCH BODY[] response"))
+ }
+ ReadResult::NeedBytes(n, reader) => result = self.feed_bytes(n, reader)?,
+ ReadResult::NeedLine(reader) => result = reader.read_line(self.read_line()?)?,
+ ReadResult::Response(response) => return Ok(response),
+ }
+ }
+ }
+
+ fn read_line(&mut self) -> Result<&mut str, nmh::Error> {
+ self.netsec.read_line()
+ }
+
+ fn write_and_flush(&mut self, s: &str) -> Result<(), nmh::Error> {
+ self.write(s)?;
+ self.netsec.flush()
+ }
+
+ fn write(&mut self, s: &str) -> Result<(), nmh::Error> {
+ self.netsec.write(s.as_bytes())
+ }
+}
+
+impl std::fmt::Debug for Session {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Session").finish()
+ }
+}
+
+impl Drop for Session {
+ fn drop(&mut self) {
+ self.netsec
+ .write("logout LOGOUT\r\n".as_bytes())
+ .unwrap_or(());
+ self.netsec.flush().unwrap_or(());
+ }
+}
+
+fn quote(s: &str) -> String {
+ let mut result = String::new();
+ result.push('"');
+ for (_, c) in s.char_indices() {
+ if c == '"' {
+ result += "\\\"";
+ } else {
+ result.push(c);
+ }
+ }
+ result.push('"');
+ result
+}
+
+impl From<nmh::Error> for Error {
+ fn from(e: nmh::Error) -> Self {
+ Self::unrecoverable(e)
+ }
+}
+
+// TODO odd home for this
+fn to_string_join<T: ToString>(values: &[T], s: &str) -> String {
+ values
+ .iter()
+ .map(|v| v.to_string())
+ .collect::<Vec<_>>()
+ .join(s)
+}
+
+#[cfg(test)]
+mod tests;
--- /dev/null
+use log::trace;
+
+use nmh::Error;
+use nmhimaptypes::Uid;
+
+use crate::imap;
+use imap::mailbox;
+use imap::protocol::{read_response, Fetch, LineReader, ReadResult, Response, ServerData, Status};
+
+/// This is the center of the message fetching system. Use it to consume FETCH respones, receiving
+/// message payloads with [MessageReader], like so:
+///
+/// ```
+/// let session: mailbox::Session;
+/// let mut f: FetchResult = session.fetch(...);
+/// let session: mailbox::Session = loop {
+/// match f {
+/// Done(session) => break session,
+/// Message(uid, seen, mut reader) => {
+/// let writer = open(uid);
+/// f = loop {
+/// match reader.read(...) {
+/// Done(f) => break f,
+/// Reading(bytes, next_reader) => {
+/// writer.write_all(bytes);
+/// reader = next_reader;
+/// }
+/// }
+/// }
+/// }
+/// }
+/// }
+/// ```
+pub enum FetchResult<'a> {
+ Message(Uid, bool, MessageReader<'a>),
+ Done(mailbox::Session),
+}
+
+/// See [FetchResult] for overview.
+pub struct MessageReader<'session> {
+ session: FetchSession<'session>,
+ bytes_remaining: usize,
+ line_reader: LineReader,
+}
+
+impl<'session> MessageReader<'session> {
+ /// Return [MessageResult::Reading] when message bytes have been received from the server,
+ /// [MessageResult::Done] with the [FetchResult] for the next message, if any remain.
+ pub fn read<'buffer>(
+ mut self,
+ buf: &'buffer mut [u8],
+ ) -> Result<MessageResult<'session, 'buffer>, Error> {
+ Ok(if self.bytes_remaining == 0 {
+ MessageResult::Done(self.session.finish_untagged_response(self.line_reader)?)
+ } else {
+ let bytes_to_read = std::cmp::min(buf.len(), self.bytes_remaining);
+ let bytes_read = self
+ .session
+ .session
+ .netsec
+ .read(&mut buf[..bytes_to_read])?;
+ self.bytes_remaining -= bytes_read;
+ MessageResult::Reading(&buf[..bytes_read], self)
+ })
+ }
+}
+
+pub enum MessageResult<'session, 'buffer> {
+ Reading(&'buffer [u8], MessageReader<'session>),
+ Done(FetchResult<'session>),
+}
+
+/// Internal state of [FetchResult].
+pub(super) struct FetchSession<'a> {
+ session: imap::Session,
+ highest_uid: u32,
+ uids_iter: &'a mut dyn std::iter::Iterator<Item = &'a u32>,
+ requests_in_flight: usize,
+}
+
+impl<'a> FetchSession<'a> {
+ pub fn start(
+ session: imap::Session,
+ highest_uid: u32,
+ uids_iter: &'a mut dyn std::iter::Iterator<Item = &'a u32>,
+ max_fetch_messages_in_flight: u32,
+ ) -> Result<FetchResult<'a>, Error> {
+ let mut fetch = Self {
+ session,
+ highest_uid,
+ uids_iter,
+ requests_in_flight: 0,
+ };
+ trace!("fetch start");
+ for _ in 0..max_fetch_messages_in_flight {
+ match fetch.uids_iter.next() {
+ None => break,
+ Some(uid) => fetch.fetch_body(*uid)?,
+ }
+ }
+ fetch.next()
+ }
+
+ fn next(mut self) -> Result<FetchResult<'a>, Error> {
+ trace!("requests_in_flight={}", self.requests_in_flight);
+ while self.requests_in_flight > 0 {
+ let mut result: ReadResult = read_response(self.session.read_line()?)?;
+ let response: Response = loop {
+ match result {
+ ReadResult::Response(r) => break r,
+ ReadResult::CommandContinuationRequest => {
+ return Err(Error::new("unexpected continue-req"))
+ }
+ ReadResult::FetchBytes(fetch_parts, bytes_to_read, line_reader) => {
+ return self.receive_body(fetch_parts, bytes_to_read, line_reader);
+ }
+ ReadResult::NeedBytes(n, reader) => {
+ result = self.session.feed_bytes(n, reader)?
+ }
+ ReadResult::NeedLine(reader) => {
+ result = reader.read_line(self.session.read_line()?)?
+ }
+ }
+ };
+ match response {
+ Response::TaggedStatus(_, Status::No(code, message)) => {
+ todo!("NO {:?} {}", code, message)
+ }
+ Response::TaggedStatus(_, Status::Bad(code, message)) => {
+ todo!("BAD {:?} {}", code, message)
+ }
+ Response::TaggedStatus(_, Status::Ok(_)) => {
+ trace!("FETCH OK requests_in_flight={}", self.requests_in_flight);
+ self.handle_tagged_response()?
+ }
+ ignored => {
+ trace!("ignoring {:?}", ignored);
+ }
+ }
+ }
+ Ok(FetchResult::Done(mailbox::Session::new(
+ self.highest_uid,
+ self.session,
+ )))
+ }
+
+ fn receive_body(
+ self,
+ fetch_parts: Vec<Fetch>,
+ bytes_remaining: usize,
+ line_reader: LineReader,
+ ) -> Result<FetchResult<'a>, Error> {
+ trace!(
+ "receive body fetch_parts={:?} bytes_to_read={} line_reader={:?}",
+ fetch_parts,
+ bytes_remaining,
+ line_reader
+ );
+ let mut opt_uid: Option<Uid> = None;
+ let mut opt_seen: Option<bool> = None;
+ for fetch_part in fetch_parts {
+ match fetch_part {
+ Fetch::Uid(uid) => match opt_uid {
+ Some(_) => todo!("UID twice!"),
+ _ => {
+ opt_uid = Some(uid);
+ }
+ },
+ Fetch::Flags(seen) => match opt_seen {
+ Some(_) => todo!("FLAGS twice!"),
+ _ => {
+ opt_seen = Some(seen);
+ }
+ },
+ }
+ }
+ let uid = match opt_uid {
+ None => todo!("assume UID comes before BODY"),
+ Some(uid) => uid,
+ };
+ let seen = match opt_seen {
+ None => todo!("assume FLAGS comes before BODY"),
+ Some(seen) => seen,
+ };
+ Ok(FetchResult::Message(
+ uid,
+ seen,
+ MessageReader {
+ session: self,
+ bytes_remaining,
+ line_reader,
+ },
+ ))
+ }
+
+ /// Finish reading the untagged "* FETCH" response; we're just ignoring anything else that comes in here.
+ fn finish_untagged_response(
+ mut self,
+ line_reader: LineReader,
+ ) -> Result<FetchResult<'a>, Error> {
+ let response: Response = {
+ let mut result: ReadResult = line_reader.read_line(self.session.read_line()?)?;
+ loop {
+ match result {
+ ReadResult::CommandContinuationRequest => {
+ return Err(Error::new("unexpected continue-req"))
+ }
+ ReadResult::FetchBytes(_, _, _) => {
+ return Err(Error::new("unexpected second FETCH BODY[] response"))
+ }
+ ReadResult::NeedBytes(n, reader) => {
+ result = self.session.feed_bytes(n, reader)?;
+ }
+ ReadResult::NeedLine(reader) => {
+ result = reader.read_line(self.session.read_line()?)?;
+ }
+ ReadResult::Response(r) => break r,
+ }
+ }
+ };
+ match response {
+ Response::ServerData(ServerData::Fetch(fetch_parts)) => {
+ if !fetch_parts.is_empty() {
+ todo!("extra parts in untagged FETCH response {:?}", fetch_parts)
+ }
+ }
+ response => todo!(
+ "supposed to be seeing the end of the FETCH response here not {:?}",
+ response
+ ),
+ }
+ self.next()
+ }
+
+ fn handle_tagged_response(&mut self) -> Result<(), Error> {
+ self.requests_in_flight -= 1;
+ if let Some(uid) = self.uids_iter.next() {
+ self.fetch_body(*uid)?
+ }
+ Ok(())
+ }
+
+ fn fetch_body(&mut self, uid: u32) -> Result<(), Error> {
+ assert_ne!(0, uid, "refusing to FETCH zero UID");
+ self.requests_in_flight += 1;
+ trace!(
+ "FETCH BODY uid={} requests_in_flight={}",
+ uid,
+ self.requests_in_flight
+ );
+ self.session.write_and_flush(&format!(
+ "f{} UID FETCH {} (UID FLAGS BODY.PEEK[])\r\n",
+ uid, uid
+ ))
+ }
+}
--- /dev/null
+use std::num::NonZeroU32;
+
+use log::trace;
+
+use nmhimaptypes::{uid, Uid, UidValidity};
+
+use crate::error::RecoverMapper;
+use crate::imap;
+use imap::protocol::{Response, ResponseCode, ServerData, Status};
+use imap::{mailbox, Flag, Mailbox};
+
+pub type Error = crate::error::RecoverableError<Session, nmh::Error>;
+
+/// Returned by [imap::Session::select].
+#[derive(Debug)]
+pub struct Selected {
+ /// Number of messages in the mailbox at SELECT time; may be zero.
+ pub message_count: u32,
+ /// Projected UID of next message to be appended to the mailbox.
+ pub uid_next: Uid,
+ /// UIDVALIDITY of mailbox at SELECT time.
+ pub uid_validity: UidValidity,
+ pub mailbox_session: Session,
+}
+
+/// See [imap::Session::select].
+// TODO Commands from imap::Session should be supported here too:
+// 6.4. Client Commands - Selected State
+// In addition to the universal commands:
+// - CAPABILITY
+// - NOOP
+// - LOGOUT
+// and the authenticated state commands:
+// - CREATE
+// - DELETE
+// - RENAME
+// - LIST
+// - APPEND
+// the following commands are valid in the selected state:
+// - CLOSE
+// - EXPUNGE
+// - SEARCH
+// - FETCH
+// - STORE
+// - COPY
+#[derive(Debug)]
+pub struct Session {
+ highest_uid: u32,
+ session: imap::Session,
+}
+
+impl Session {
+ /// Same as [imap::Session::list].
+ pub fn list(self, mailbox_root: &str) -> Result<(Vec<Mailbox>, Self)> {
+ Ok({
+ let (session, mut rm) = self.recover_mapper();
+ let (mailboxes, session) = session.list(mailbox_root).map_err(|e| rm.map(e))?;
+ (
+ mailboxes,
+ Self {
+ highest_uid: rm.ok(),
+ session,
+ },
+ )
+ })
+ }
+
+ /// Return the set of all UIDs in the mailbox.
+ pub fn search_all(self) -> Result<(uid::Set, Self)> {
+ search(self, "ALL")
+ }
+
+ /// Return set of UIDs of messages lacking [Flag::Seen].
+ pub fn search_unseen(self) -> Result<(uid::Set, Self)> {
+ search(self, "NOT SEEN")
+ }
+
+ /// Close this session and open one for the named `mailbox`.
+ pub fn select(self, mailbox: &mailbox::Name) -> Result<Selected> {
+ let (session, mut rm) = self.recover_mapper();
+ session.select(mailbox).map_err(|e| rm.map(e))
+ }
+
+ /// Close this session, returning to the authenticated state.
+ pub fn close(self) -> Result<imap::Session> {
+ // TODO Can we test this failing? RFC 3501 says CLOSE may return BAD but it isn't clear why.
+ Ok(self.session.close()?)
+ }
+
+ // => UID COPY 2:4 dest
+ // <= 1 OK [COPYUID 1613513882 3 2] UID COPY completed
+ /// Copy the message in this mailbox identified by `uid` to the mailbox named `dst_mailbox`.
+ /// Return the new message's UID and the destination mailbox's UIDVALIDITY, which may have changed.
+ pub fn copy(
+ mut self,
+ uid: Uid,
+ dst_mailbox: &mailbox::Name,
+ ) -> Result<((UidValidity, Uid), Self)> {
+ self.session
+ .write_and_flush(&format!("1 UID COPY {} {}\r\n", uid, dst_mailbox))?;
+ loop {
+ match self.session.read_response()? {
+ // TODO Bail out (unrecoverable) if UIDVALIDITY changed; tell user to fix with inc.
+ Response::TaggedStatus(_, Status::Ok(ResponseCode::CopyUid(uidvalidity, uid))) => {
+ break Ok(((uidvalidity, uid), self));
+ }
+ Response::TaggedStatus(_, Status::Ok(_)) => {
+ todo!("COPYUID response too small")
+ }
+ Response::TaggedStatus(_, _) => todo!("non-OK status"),
+ ignored => {
+ trace!("ignoring {:?}", ignored);
+ }
+ }
+ }
+ }
+
+ // TODO `uids` should be &[Uid] or &uid::Set
+ // TODO returning silly unit to ease usage of error recovery macros; make them smarter
+ /// Add `flags` on messages listed in `uids`. May not be persisted until
+ /// this session is closed by [Session::close] or [Session::select].
+ pub fn add_flags(mut self, uids: &str, flags: &[Flag]) -> Result<((), Self)> {
+ self.session.write_and_flush(&format!(
+ "1 UID STORE {} +FLAGS.SILENT ({})\r\n",
+ uids,
+ super::to_string_join(flags, " ")
+ ))?;
+ loop {
+ match self.session.read_response()? {
+ Response::TaggedStatus(_, Status::Ok(_)) => break Ok(((), self)),
+ Response::TaggedStatus(_, _) => todo!("non-OK status"),
+ ignored => {
+ trace!("ignoring {:?}", ignored);
+ }
+ }
+ }
+ }
+
+ // TODO `uids` should be &[Uid] or &uid::Set
+ // TODO returning silly unit to ease usage of error recovery macros; make them smarter
+ /// Delete `flags` on messages listed in `uids`. May not be persisted
+ /// until this session is closed by [Session::close] or [Session::select].
+ pub fn del_flags(mut self, uids: &str, flags: &[Flag]) -> Result<((), Self)> {
+ self.session.write_and_flush(&format!(
+ "1 UID STORE {} -FLAGS.SILENT ({})\r\n",
+ uids,
+ super::to_string_join(flags, " ")
+ ))?;
+ loop {
+ match self.session.read_response()? {
+ Response::TaggedStatus(_, Status::Ok(_)) => break Ok(((), self)),
+ Response::TaggedStatus(_, _) => todo!("non-OK status"),
+ ignored => {
+ trace!("ignoring {:?}", ignored);
+ }
+ }
+ }
+ }
+
+ // TODO currently unrecoverable because we might have an error with further response coming down
+ // that would need processing and it's easier just to abandon that connection. If we really cared,
+ // we could recover in the case where no further requests are in flight but is it worth the complexity...
+ /// Send up to `max_fetch_messages_in_flight` message FETCH requests, and keep that many FETCH
+ /// requests in flight at a time, as long as `uids_iter` does not run out.
+ /// See [imap::FetchResult] for information on fetching messages.
+ pub fn fetch<'a>(
+ self,
+ max_fetch_messages_in_flight: NonZeroU32,
+ uids_iter: &'a mut dyn std::iter::Iterator<Item = &'a u32>,
+ ) -> std::result::Result<imap::FetchResult<'a>, nmh::Error> {
+ super::fetch::FetchSession::start(
+ self.session,
+ self.highest_uid,
+ uids_iter,
+ max_fetch_messages_in_flight.into(),
+ )
+ }
+
+ pub(super) fn new(highest_uid: u32, session: imap::Session) -> Self {
+ Self {
+ highest_uid,
+ session,
+ }
+ }
+
+ fn recover_mapper<'a>(self) -> (imap::Session, RecoverMapper<'a, imap::Session, Self, u32>) {
+ (
+ self.session,
+ RecoverMapper::new(
+ &|session, highest_uid| Self {
+ highest_uid,
+ session,
+ },
+ self.highest_uid,
+ ),
+ )
+ }
+}
+
+type Result<T> = std::result::Result<T, Error>;
+
+fn search(mut mailbox_session: Session, search: &str) -> Result<(uid::Set, Session)> {
+ mailbox_session
+ .session
+ .write_and_flush(&format!(
+ "1 UID SEARCH{} {}\r\n",
+ if mailbox_session.session.esearch {
+ " RETURN ()"
+ } else {
+ ""
+ },
+ search
+ ))
+ .map_err(Error::unrecoverable)?;
+ let mut result = uid::Set::new();
+ let status: Status = {
+ // https://www.imapwiki.org/ClientImplementation/Search says we need to handle
+ // the SEARCH results being split across multiple untagged responses.
+ loop {
+ match mailbox_session
+ .session
+ .read_response()
+ .map_err(Error::unrecoverable)?
+ {
+ Response::ServerData(ServerData::ESearch(uids)) => {
+ result.append(uids);
+ }
+ Response::ServerData(ServerData::Search(uids)) => {
+ result.append(uids);
+ }
+ // <= $tag OK UID SEARCH completed
+ Response::TaggedStatus(_, status) => break status,
+ ignored => {
+ log::trace!("ignoring {:?}", ignored);
+ }
+ }
+ }
+ };
+
+ match status {
+ Status::No(code, message) => {
+ // TODO pretty this up; can `code` be interpreted?
+ Err(Error::recoverable(
+ nmh::Error::new(format!("{:?} {}", code, message)),
+ mailbox_session,
+ ))
+ }
+ Status::Bad(code, message) => {
+ // TODO does BAD always mean our bug or can it be user error?
+ Err(Error::unrecoverable(nmh::Error::new(format!(
+ "{:?} {}",
+ code, message
+ ))))
+ }
+ Status::Ok(_) => Ok((result, mailbox_session)),
+ }
+}
+
+impl From<nmh::Error> for Error {
+ fn from(e: nmh::Error) -> Self {
+ Self::unrecoverable(e)
+ }
+}
--- /dev/null
+use super::*;
+
+#[test]
+fn embedded_quote() {
+ let unquoted = r#"a "b" c"#;
+ let quoted = r#""a \"b\" c""#;
+ assert_eq!(quoted, quote(unquoted));
+}
--- /dev/null
+use std::io::Write;
+use std::os::unix::ffi::OsStrExt;
+
+// TODO make this a macro so we capture the line/file where this is called?
+/// Log at `level` parts of `s`.
+///
+/// First logs all of `s` and then logs only the parts between `start`
+/// and `one_past_end`, like so:
+///
+/// ```text
+/// (UID 6 FLAGS (\Seen) BODY[] {91}
+/// ' FLAGS (\Seen) BODY[] {91}'
+/// ```
+pub fn log_string_parts(level: log::Level, s: &str, start: usize, one_past_end: usize) {
+ if log::log_enabled!(level) {
+ log::log!(level, " {}", s);
+ log::log!(
+ level,
+ "{}'{}{}'",
+ std::str::from_utf8(&vec![32; start]).expect("32 (SPC) is valid UTF-8"),
+ &s[start..one_past_end],
+ std::str::from_utf8(&vec![32; s.len() - one_past_end])
+ .expect("32 (SPC) is valid UTF-8"),
+ );
+ }
+}
+
+/// Log at [log::Level::Error] `e`, and its [std::error::Error::source] if any, and its... and so on.
+pub fn error(mut e: &dyn std::error::Error) {
+ let mut i = 0;
+ log::error!("{}: {:?}", i, e);
+ loop {
+ i += 1;
+ match e.source() {
+ Some(source) => {
+ e = source;
+ log::error!("{}: {:?}", i, e);
+ }
+ None => break,
+ }
+ }
+}
+
+/// Redirect the standard error (file descriptor 2) to the file named `path`.
+pub fn redirect_stderr_to(path: std::path::PathBuf) -> Result<(), String> {
+ let fd = {
+ let cs = std::ffi::CString::new(path.as_os_str().as_bytes())
+ .expect("Path not to contain null byte");
+ let cp = cs.as_bytes_with_nul().as_ptr() as *const libc::c_char;
+ unsafe { libc::open(cp, libc::O_APPEND | libc::O_CREAT | libc::O_WRONLY, 0o600) }
+ };
+ if fd < 0 {
+ return Err(format!(
+ "open({}): {}",
+ path.display(),
+ std::io::Error::last_os_error()
+ ));
+ }
+ if unsafe { libc::dup2(fd, 2) } < 0 {
+ return Err(format!(
+ "dup2({}, stderr): {}",
+ path.display(),
+ std::io::Error::last_os_error()
+ ));
+ }
+ if unsafe { libc::close(fd) } < 0 {
+ return Err(format!(
+ "close({}): {}",
+ path.display(),
+ std::io::Error::last_os_error()
+ ));
+ }
+ Ok(())
+}
+
+/// Install a [log::Log] implementation ([Logger]) based on the `mh-profile(5)` and which
+/// Panics if it cannot persist logs to configured files.
+pub fn install(prefixed: &nmh::profile::Prefixed) -> Result<(), String> {
+ let default = prefixed
+ .get("log-level")
+ .map(|level_s| level_s.parse::<log::LevelFilter>())
+ .unwrap_or(Ok(log::LevelFilter::Off))
+ .map_err(|e| e.to_string())?;
+ let mut max = default;
+ let mut module_to_level = std::collections::HashMap::new();
+ if let Some(modules) = prefixed.get("log-modules") {
+ for module in nmh::hacks::split(modules) {
+ if let Some(level_s) = prefixed.get(&format!("log-module-{}", module)) {
+ let level = level_s
+ .parse::<log::LevelFilter>()
+ .map_err(|e| e.to_string())?;
+ max = std::cmp::max(level, max);
+ module_to_level.insert(module.to_owned(), level);
+ }
+ }
+ }
+
+ let mut level_to_path = std::collections::HashMap::new();
+ let mut last: Option<std::path::PathBuf> = None;
+ for level in &[
+ log::Level::Trace,
+ log::Level::Debug,
+ log::Level::Info,
+ log::Level::Warn,
+ log::Level::Error,
+ ] {
+ // TODO need to support relative and absolute in all paths read from profile
+ match prefixed.get(&format!("log-{}-to", level)) {
+ Some(path_s) => {
+ let buf = std::path::Path::new(path_s).to_owned();
+ level_to_path.insert(*level, buf.clone());
+ last = Some(buf)
+ }
+ _ => {
+ if let Some(path) = &last {
+ level_to_path.insert(*level, path.to_owned());
+ }
+ }
+ }
+ }
+
+ if default > log::LevelFilter::Off || !module_to_level.is_empty() {
+ if level_to_path.is_empty() {
+ return Err("logging requested but no log files specified".to_owned());
+ }
+ log::set_boxed_logger(Box::new(Logger {
+ default,
+ module_to_level,
+ level_to_path,
+ pid: std::process::id(),
+ }))
+ .map_err(|e| e.to_string())?;
+ log::set_max_level(max);
+ }
+
+ Ok(())
+}
+
+struct Logger {
+ default: log::LevelFilter,
+ module_to_level: std::collections::HashMap<String, log::LevelFilter>,
+ level_to_path: std::collections::HashMap<log::Level, std::path::PathBuf>,
+ pid: u32,
+}
+
+impl log::Log for Logger {
+ fn enabled(&self, m: &log::Metadata) -> bool {
+ m.level() <= self.default
+ || self
+ .module_to_level
+ .get(m.target())
+ .map(|level| m.level() <= *level)
+ .unwrap_or(false)
+ }
+
+ fn log(&self, r: &log::Record) {
+ if !self.enabled(r.metadata()) {
+ return;
+ }
+ if let Some(path) = &self.level_to_path.get(&r.metadata().level()) {
+ let mut file: std::fs::File = std::fs::OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(path)
+ .unwrap();
+ file.write_fmt(format_args!(
+ "{} {} {} {}:{} {}\n",
+ unsafe { nmh::dtimenow() }.to_str().unwrap(),
+ self.pid,
+ r.level(),
+ r.file().unwrap_or("UNKNOWN"),
+ r.line().unwrap_or(0),
+ r.args()
+ ))
+ .unwrap();
+ file.sync_all().unwrap()
+ }
+ }
+
+ fn flush(&self) {}
+}
--- /dev/null
+use log::trace;
+
+use crate::regex::Regex;
+
+#[derive(Debug)]
+pub struct MailboxRules(Vec<Rule>);
+
+/// These rules determine whether to include or exclude mailboxes from `inc(1)`.
+impl MailboxRules {
+ pub fn new() -> Self {
+ Self(vec![])
+ }
+
+ /// Mark mailboxes whose names match `pattern` for inclusion.
+ pub fn include(&mut self, pattern: Regex) {
+ self.0.push(Rule {
+ pattern,
+ include: true,
+ })
+ }
+
+ /// Mark mailboxes whose names match `pattern` for exclusion.
+ pub fn exclude(&mut self, pattern: Regex) {
+ self.0.push(Rule {
+ pattern,
+ include: false,
+ })
+ }
+
+ /// Return whether the mailbox named `mailbox` should be be included.
+ pub fn included(&self, mailbox: &str) -> bool {
+ let mut include = true;
+ for rule in &self.0 {
+ if rule.pattern.matches(mailbox) {
+ trace!(
+ "MailboxRules::included was {} now {}",
+ include,
+ rule.include
+ );
+ include = rule.include;
+ } else {
+ trace!("MailboxRules::included leaving it {}", include);
+ }
+ }
+ trace!(
+ "MailboxRules::included({}, {}) -> {}",
+ mailbox,
+ self.0.len(),
+ include
+ );
+ include
+ }
+}
+
+#[derive(Debug)]
+struct Rule {
+ pattern: Regex,
+ include: bool,
+}
--- /dev/null
+mod cf;
+mod error;
+mod logging;
+mod mailbox_rule;
+mod regex;
+mod run;
+
+// pub just to hush cargo doc ¯\_(ツ)_/¯
+pub mod imap;
+pub mod manager;
+
+fn main() {
+ if std::mem::size_of::<i32>() != std::mem::size_of::<std::os::raw::c_int>() {
+ todo!("i32 != c_int not yet supported");
+ }
+
+ // Create a new session and close stdin.
+ unsafe {
+ libc::setsid();
+ if libc::close(0) < 0 {
+ libc::perror("close(0)".as_ptr() as *const libc::c_char);
+ std::process::exit(2)
+ }
+ }
+ // Leaving stderr open until we have accepted a connection through which we can instead report errors.
+ // Leaving stdout open so we can tell the calling program we're listening.
+
+ let (context, name) = parse_args(std::env::args());
+ if let Err(e) = run::configure_and_run(&context, name) {
+ eprintln!("{}", e);
+ std::process::exit(2)
+ }
+}
+
+fn parse_args(mut args: std::env::Args) -> (nmh::Context, String) {
+ (
+ nmh::init(std::ffi::CString::new(args.next().unwrap()).unwrap()),
+ args.next().unwrap(),
+ )
+}
--- /dev/null
+//! Where everything is tied together and IMAP mailboxes become MH folders.
+//!
+//! [Manager] delegates much of its work to various sub-modules, and most of
+//! what you see in here is for their usage.
+//!
+//! # Error recovery
+//! The following macros are available to facilitate recovering from errors.
+//! - [recover_authenticated]
+//! - [recover_selected]
+
+use std::io::Read as _;
+
+use log::trace;
+
+use managed_folders as nmf;
+use nmh::{folder, message};
+use nmhimapstore::{
+ FolderPathMapper, FolderStore, ReadOnlyTransaction, ReadTransaction, WriteTransaction,
+};
+use nmhimaptypes::{Uid, UidValidity};
+
+use crate::imap;
+use imap::{mailbox, Flag, Session};
+
+mod command;
+mod mapper;
+mod pick;
+
+pub type Error = crate::error::RecoverableError<Manager, nmh::Error>;
+
+pub use mapper::FolderMailboxMapper;
+
+/// The best manager of folders and IMAP mailboxes nmh has ever seen.
+#[derive(Debug)]
+pub struct Manager {
+ fmm: FolderMailboxMapper,
+ fpm: FolderPathMapper,
+ state: State,
+ cf: crate::cf::Manager,
+ buf: Vec<u8>,
+}
+
+/// Use this to recover from errors in the authenticated state. When passed a [Result] of:
+/// - [Ok] - passes it along
+/// - [imap::Error] with a recoverable [imap::Session] - reconstructs the [Manager] with the session
+/// - [imap::Error] but not recoverable - passes the error along
+///
+/// Parameters
+/// - `$mparts` is [ManagerParts] whose [ManagerParts::recover_authenticated] to use to recover
+/// [imap::Session] from [imap::Error] into the [Manager] of an [Error].
+/// - `$expression` evaluates to [imap::Error]
+#[macro_export]
+macro_rules! recover_authenticated {
+ ($mparts:ident, $expression:expr) => {{
+ let mut rm = $mparts.recover_authenticated();
+ ($expression.map_err(|e| rm.map(e))?, rm.ok())
+ }};
+}
+
+/// Use this to recover from errors in the selected state. When passed a [Result] of:
+/// - [Ok] - passes it along
+/// - [imap::mailbox::Error] with a recoverable [imap::mailbox::Session] - reconstructs the [Manager] with the session
+/// - [imap::mailbox::Error] but not recoverable - passes the error along
+///
+/// Parameters
+/// - `$mparts` is [ManagerParts] whose [ManagerParts::recover_selected] to use along with `$selected` to
+/// recover [imap::Session] from [imap::Error] into the [Manager] of an [Error].
+/// - `$mailbox_name` is the name of the mailbox to go into [State::Selected]; used with `$mparts`
+/// - `$selected` is the [SelectedParts] from the [imap::mailbox::Selected]; used with `$mparts`
+/// - `$expression` evaluates to [imap::mailbox::Error]
+#[macro_export]
+macro_rules! recover_selected {
+ ($mparts:ident, $mailbox_name:ident, $selected:ident, $expression:expr) => {{
+ let mut rm = $mparts.recover_selected($mailbox_name, $selected);
+ let (result, mailbox_session) = $expression.map_err(|e| rm.map(e))?;
+ let (mparts, mailbox_name, selected_parts) = rm.ok();
+ (
+ result,
+ mailbox_name,
+ selected_parts.construct(mailbox_session),
+ mparts,
+ )
+ }};
+}
+
+impl Manager {
+ pub fn new(
+ fmm: FolderMailboxMapper,
+ fpm: FolderPathMapper,
+ session: imap::Session,
+ cf: crate::cf::Manager,
+ ) -> Self {
+ Self {
+ fmm,
+ fpm,
+ state: State::Authenticated(session),
+ cf,
+ buf: vec![0; 8192],
+ }
+ }
+
+ pub fn run(
+ mut self,
+ command: nmf::Command,
+ ) -> Result<(Self, nmf::FolderManagerProtocol), String> {
+ Ok(match command {
+ nmf::Command::Quit => todo!(),
+
+ nmf::Command::Inc(mut response, command) => {
+ match command::inc::inc(self, &mut response, &command.folder) {
+ Err(recoverable) => {
+ crate::logging::error(&recoverable);
+ let message =
+ format!("failed to inc folder {}: {}", command.folder, recoverable);
+ match recoverable.recover() {
+ Ok((recovered, _)) => (recovered, response.error(&message)?),
+ Err(e) => {
+ response.error(&message)?;
+ return Err(e.to_string());
+ }
+ }
+ }
+ Ok(new_self) => (new_self, response.ok()?),
+ }
+ }
+
+ nmf::Command::MakeDraft(response, command) => {
+ let (msgnum, new_self) = self.make_draft(command).unwrap();
+ (new_self, response.ok(msgnum)?)
+ }
+
+ // All the rest use EmptyResponse.
+ nmf::Command::No(response) => (self, response.ok()?),
+ nmf::Command::Folder(_, _) => todo!(),
+ nmf::Command::MakeFolder(response, command) => {
+ match self.make_folder(&command.folder) {
+ Err(recoverable) => {
+ crate::logging::error(&recoverable);
+ let message = format!(
+ "failed to create folder {}: {}",
+ command.folder, recoverable
+ );
+ match recoverable.recover() {
+ Ok((recovered, _)) => (recovered, response.error(&message)?),
+ Err(e) => {
+ response.error(&message)?;
+ return Err(e.to_string());
+ }
+ }
+ }
+ Ok(new_self) => (new_self, response.ok()?),
+ }
+ }
+ nmf::Command::MarkAdd(response, command) => {
+ self = command::mark::add(self, command).unwrap();
+ (self, response.ok()?)
+ }
+ nmf::Command::MarkDel(response, command) => {
+ self = command::mark::delete(self, command).unwrap();
+ (self, response.ok()?)
+ }
+ nmf::Command::Pick(response, command) => {
+ self = command::pick::pick(self, command).unwrap();
+ (self, response.ok()?)
+ }
+ nmf::Command::RefileFile(response, command) => {
+ (self.refile_file(command).unwrap(), response.ok()?)
+ }
+ nmf::Command::RefileMove(response, command) => {
+ (self.refile_move(command).unwrap(), response.ok()?)
+ }
+ nmf::Command::Rmf(response, command) => (self.rmf(command).unwrap(), response.ok()?),
+ nmf::Command::Rmm(response, command) => (self.rmm(command).unwrap(), response.ok()?),
+ nmf::Command::UpdateDraft(response, command) => {
+ (self.update_draft(command).unwrap(), response.ok()?)
+ }
+ })
+ }
+
+ /// Destruct into session `State` and everything else - used for error recovery.
+ pub fn destruct(self) -> (State, ManagerParts) {
+ (
+ self.state,
+ ManagerParts {
+ fmm: self.fmm,
+ fpm: self.fpm,
+ cf: self.cf,
+ buf: self.buf,
+ },
+ )
+ }
+
+ // TODO the rest of these methods are some of the oldest and most in need of work
+ // TODO move these out
+
+ fn make_draft(
+ mut self,
+ command: nmf::command::Folder,
+ ) -> Result<(message::Number, Self), Error> {
+ // TODO if drafts folder doesn't exist:
+ // <= * OK [TRYCREATE] Must create mailbox before append
+ // <= 1 NO [TRYCREATE] Must create mailbox before append
+ // TODO folder is nmh folder! map to imap mailbox name
+ let mailbox: mailbox::Name = self.fmm.folder_name_to_mailbox(&command.folder)?;
+ let (state, mparts) = self.destruct();
+ let session = match state {
+ State::Authenticated(session) => session,
+ State::Selected(_, selected) => match selected.mailbox_session.close() {
+ Err(e) => return Err(Error::unrecoverable(nmh::Error::wrap(e))),
+ Ok(session) => {
+ trace!("comp CLOSEd into Authenticated state for APPEND (TODO don't)");
+ session
+ }
+ },
+ };
+ let (((uidvalidity, uid), session), mparts) = recover_authenticated!(
+ mparts,
+ session.append(
+ &mailbox,
+ &[Flag::Draft, Flag::Seen,],
+ // TODO put something else here. It's not that important; the
+ // user is composing a draft and we'll upload that as soon as
+ // they're done. But if they should walk away, it would be good
+ // not to leave a weird message like this behind for them to be
+ // surprised by. Maybe we shouldn't be appending a placeholder
+ // message at all, but should instead be able to reserve a
+ // pending message number which doesn't yet have a UID.
+ "From:me\r\nTo:you\r\n\r\n\r\n",
+ )
+ );
+ self = mparts.authenticated(session);
+
+ let store: FolderStore = store::open(&self.fpm, &command.folder, uidvalidity)?;
+ let mut txn: WriteTransaction = store.write_transaction()?;
+ let msgnum: message::Number = txn.store_message(uid)?;
+ txn.commit()?;
+ Ok((msgnum, self))
+ }
+
+ fn make_folder(self, folder_name: &folder::Name) -> Result<Self, Error> {
+ let mailbox: mailbox::Name = self.fmm.folder_name_to_mailbox(folder_name)?;
+ let (state, mparts) = self.destruct();
+ let session = match state {
+ State::Authenticated(session) => session,
+ State::Selected(_, selected) => match selected.mailbox_session.close() {
+ Err(e) => return Err(Error::unrecoverable(nmh::Error::wrap(e))),
+ Ok(session) => {
+ trace!("CLOSEd into Authenticated state for CREATE (TODO don't)");
+ session
+ }
+ },
+ };
+ let (session, mparts) = recover_authenticated!(mparts, session.create(mailbox.as_str()));
+ let (selected, mparts): (mailbox::Selected, ManagerParts) =
+ recover_authenticated!(mparts, session.select(&mailbox));
+ FolderStore::create(
+ &mparts.fpm,
+ folder_name,
+ selected.uid_next,
+ selected.uid_validity,
+ )?;
+ // TODO don't CLOSE
+ Ok(mparts.authenticated(selected.mailbox_session.close().map_err(nmh::Error::wrap)?))
+ }
+
+ fn delete_message(
+ mut self,
+ folder: &folder::Name,
+ msgnum: message::Number,
+ ) -> Result<Self, Error> {
+ // 0. Open mailbox
+ let (state, mparts) = self.destruct();
+ let session = match state {
+ State::Authenticated(session) => session,
+ State::Selected(_, selected) => match selected.mailbox_session.close() {
+ Err(e) => return Err(Error::unrecoverable(nmh::Error::wrap(e))),
+ Ok(session) => {
+ trace!("rmm CLOSEd into Authenticated state for STORE (TODO check first)");
+ session
+ }
+ },
+ };
+
+ let mailbox_name = mparts.fmm.folder_name_to_mailbox(folder)?;
+ let (selected, mparts): (mailbox::Selected, ManagerParts) =
+ recover_authenticated!(mparts, session.select(&mailbox_name));
+
+ // 1. Resolve message ranges to UID ranges
+ let store: FolderStore = store::open(&mparts.fpm, folder, selected.uid_validity)?;
+ let uid: Uid = store
+ .read_transaction()?
+ .lookup_uid(msgnum)?
+ .ok_or_else(|| {
+ todo!("recoverable error: user-specified message does not exist");
+ })
+ .unwrap();
+
+ // 2. STORE + EXPUNGE on deleting all UIDs
+ let (mailbox_session, selected_parts) = SelectedParts::destruct(selected);
+ let (_, _, selected, mparts) = recover_selected!(
+ mparts,
+ mailbox_name,
+ selected_parts,
+ mailbox_session.add_flags(&uid.to_string(), &[Flag::Deleted])
+ );
+ // TODO EXPUNGE rather than CLOSE
+ self = mparts.authenticated(selected.mailbox_session.close().map_err(nmh::Error::wrap)?);
+
+ // 3. Delete all msgnum/UID pairs from db in one transaction
+ store.delete_message(msgnum, uid)?;
+
+ Ok(self)
+ }
+
+ fn copy_message(
+ mut self,
+ src_folder: &folder::Name,
+ src: message::Number,
+ dst_folder: &folder::Name,
+ ) -> Result<Self, Error> {
+ let (state, mparts) = self.destruct();
+ let session = match state {
+ State::Authenticated(session) => session,
+ State::Selected(_, selected) => match selected.mailbox_session.close() {
+ Err(e) => return Err(Error::unrecoverable(nmh::Error::wrap(e))),
+ Ok(session) => {
+ trace!("rmm CLOSEd into Authenticated state for COPY (TODO check first)");
+ session
+ }
+ },
+ };
+ let src_mailbox_name: mailbox::Name = mparts.fmm.folder_name_to_mailbox(src_folder)?;
+ let (src_selected, mparts): (mailbox::Selected, ManagerParts) =
+ recover_authenticated!(mparts, session.select(&src_mailbox_name));
+
+ let src_uid: Uid = {
+ let src_store: FolderStore =
+ store::open(&mparts.fpm, src_folder, src_selected.uid_validity)?;
+ let txn: ReadOnlyTransaction = src_store.read_transaction()?;
+ match txn.lookup_uid(src)? {
+ None => {
+ return Err(Error::recoverable(
+ nmh::Error::new(format!("message {} doesn't exist", src)),
+ mparts.selected(src_mailbox_name, src_selected),
+ ));
+ }
+ Some(v) => v,
+ }
+ };
+
+ // 1. COPY
+ let dst_mailbox_name: mailbox::Name = mparts.fmm.folder_name_to_mailbox(dst_folder)?;
+ let (src_mailbox_session, src_selected_parts) = SelectedParts::destruct(src_selected);
+ let ((dst_uidvalidity, dst_uid), _, src_selected, mparts) = recover_selected!(
+ mparts,
+ src_mailbox_name,
+ src_selected_parts,
+ src_mailbox_session.copy(src_uid, &dst_mailbox_name)
+ );
+ self = mparts.authenticated(
+ // TODO need to CLOSE?
+ src_selected
+ .mailbox_session
+ .close()
+ .map_err(nmh::Error::wrap)?,
+ );
+
+ // 2. Find latest msgnum from target folder msg2uid table
+ // TODO check UIDVALIDITY later when we get it
+ // TODO move to FolderStore
+ let dst_store: FolderStore = store::open(&self.fpm, dst_folder, dst_uidvalidity)?;
+ let mut dst_txn: WriteTransaction = dst_store.write_transaction()?;
+ // TODO don't want highest UID; worth having msgnum-only form?
+ let dst_msgnum: message::Number = match dst_txn.highest()? {
+ None => message::Number::one(),
+ Some((_, mut msgnum)) => {
+ msgnum.increment();
+ msgnum
+ }
+ };
+
+ // 3. Link msg file into target folder
+ // TODO std::fs::remove_file(dst_msg_path)
+ std::fs::hard_link(
+ self.fpm.msg_path(src_folder, src),
+ self.fpm.msg_path(dst_folder, dst_msgnum),
+ )
+ .unwrap();
+
+ // TODO check dst_store UIDVALIDITY against that returned by mailbox::Session::copy.
+
+ // 4. Update tables
+ // Tested by test-live:'mark unseen after refile'.
+ dst_txn.set_uid_msgnum(dst_uid, dst_msgnum)?;
+
+ // TODO make WriteTransaction & PendingDelete panic in Drop when not committed
+ dst_txn.commit()?;
+
+ Ok(self)
+ }
+
+ fn rmf(mut self, command: nmf::command::Folder) -> Result<Self, Error> {
+ // 1. Attempt mailbox deletion first so we don't delete local cache pointlessly.
+ let session = match self.state {
+ State::Authenticated(session) => session,
+ State::Selected(_, selected) => match selected.mailbox_session.close() {
+ Err(e) => return Err(Error::unrecoverable(nmh::Error::wrap(e))),
+ Ok(session) => {
+ trace!("rmf CLOSEd into Authenticated state for DELETE (TODO don't)");
+ session
+ }
+ },
+ };
+ self.state = State::Authenticated(
+ session
+ .delete(&self.fmm.folder_name_to_mailbox(&command.folder)?)
+ .map_err(nmh::Error::wrap)?,
+ );
+ // 2. Delete local files. If any foreign objects in folder, leave them.
+ nmhimapstore::delete(&self.fpm, &command.folder)?;
+ Ok(self)
+ }
+
+ fn rmm(self, command: nmf::command::Rmm) -> Result<Self, Error> {
+ // TODO message range parsing for removing more than one message
+ let msgnum: message::Number = command.msgs[0].parse()?;
+ self.delete_message(&command.folder, msgnum)
+ }
+
+ fn refile_file(self, command: nmf::command::RefileFile) -> Result<Self, Error> {
+ // 1. read file
+ let contents: String = {
+ let mut file = std::fs::File::open(command.file_path).unwrap();
+ let mut contents = String::new();
+ file.read_to_string(&mut contents).unwrap();
+ contents
+ };
+
+ // 2. APPEND to mailbox.
+ let (state, mparts) = self.destruct();
+ let session = match state {
+ State::Authenticated(session) => session,
+ State::Selected(_, selected) => match selected.mailbox_session.close() {
+ Err(e) => return Err(Error::unrecoverable(nmh::Error::wrap(e))),
+ Ok(session) => {
+ trace!("refile CLOSEd into Authenticated state for APPEND (TODO don't)");
+ session
+ }
+ },
+ };
+ let mailbox_name: mailbox::Name = mparts.fmm.folder_name_to_mailbox(&command.dst_folder)?;
+ let (((dst_uidvalidity, dst_uid), session), mparts) = recover_authenticated!(
+ mparts,
+ session.append(
+ &mailbox_name,
+ &[Flag::Seen],
+ &contents.lines().collect::<Vec<_>>().join("\r\n"),
+ )
+ );
+
+ // 3. Append to folder.
+ let mut writer: nmhimapstore::MessageWriter =
+ store::open(&mparts.fpm, &command.dst_folder, dst_uidvalidity)?
+ .append(&mparts.fpm, &command.dst_folder)?
+ .append(dst_uid)?;
+ writer.write_all(contents.as_bytes())?;
+ let (_, new_appender) = writer.close()?;
+ new_appender.close();
+
+ Ok(mparts.authenticated(session))
+ }
+
+ fn refile_move(self, command: nmf::command::RefileMove) -> Result<Self, Error> {
+ // TODO message range parsing for refiling more than one message
+ let msgnum: message::Number = command.msgs[0].parse()?;
+ self.copy_message(&command.src_folder, msgnum, &command.dst_folder)?
+ .delete_message(&command.src_folder, msgnum)
+ }
+
+ fn update_draft(self, command: nmf::command::UpdateDraft) -> Result<Self, Error> {
+ // TODO Return error to client rather than let Number::from panic.
+ let contents: String =
+ std::fs::read_to_string(self.fpm.msg_path(&command.folder, command.msgnum))
+ .unwrap()
+ .lines()
+ .collect::<Vec<_>>()
+ .join("\r\n");
+
+ // 1. let new_uid = APPEND new contents
+ let mailbox: mailbox::Name = self.fmm.folder_name_to_mailbox(&command.folder)?;
+ let (state, mparts) = self.destruct();
+ let session = match state {
+ State::Authenticated(session) => session,
+ State::Selected(_, selected) => match selected.mailbox_session.close() {
+ Err(e) => return Err(Error::unrecoverable(nmh::Error::wrap(e))),
+ Ok(session) => {
+ trace!("update-draft CLOSEd into Authenticated state for APPEND (TODO don't)");
+ session
+ }
+ },
+ };
+
+ let (((uidvalidity, new_uid), session), mparts) = recover_authenticated!(
+ mparts,
+ session.append(&mailbox, &[Flag::Draft, Flag::Seen,], &contents)
+ );
+
+ // If we crash here, the next inc will pull in the updated draft as a
+ // new message. Locally, old and new will be identical, though the
+ // old draft won't match what's on the server.
+ // TODO test this
+
+ // 2. let old_uid = lookup(msg)
+ // TODO check UIDVALIDITY later when we get it
+ let store: FolderStore = store::open(&mparts.fpm, &command.folder, uidvalidity)?;
+ let mut txn: WriteTransaction = store.write_transaction()?;
+
+ // 3. Update msg->uid & uid->msg tables for new_uid
+ let old_uid = txn.replace_uid_msgnum(new_uid, command.msgnum)?;
+ txn.commit()?;
+
+ // If we crash here, the next inc will pull in a copy of the OLD draft as a new message.
+ // TODO test this
+
+ // 4. Delete the old message.
+ let (selected, mparts): (mailbox::Selected, ManagerParts) =
+ recover_authenticated!(mparts, session.select(&mailbox));
+ let (mailbox_session, selected_parts) = SelectedParts::destruct(selected);
+ let (_, _, selected, mparts) = recover_selected!(
+ mparts,
+ mailbox,
+ selected_parts,
+ mailbox_session.add_flags(&old_uid.to_string(), &[Flag::Deleted])
+ );
+ // TODO EXPUNGE rather than CLOSE
+ Ok(mparts.authenticated(selected.mailbox_session.close().map_err(nmh::Error::wrap)?))
+ }
+}
+
+/// [mailbox::Selected] without [mailbox::Session] - used for error recovery.
+pub struct SelectedParts {
+ message_count: u32,
+ uid_next: Uid,
+ uid_validity: UidValidity,
+}
+impl SelectedParts {
+ /// Destruct `selected` into session and everything else.
+ fn destruct(selected: mailbox::Selected) -> (mailbox::Session, Self) {
+ (
+ selected.mailbox_session,
+ Self {
+ message_count: selected.message_count,
+ uid_next: selected.uid_next,
+ uid_validity: selected.uid_validity,
+ },
+ )
+ }
+
+ /// Put it back together.
+ fn construct(self, mailbox_session: mailbox::Session) -> mailbox::Selected {
+ mailbox::Selected {
+ mailbox_session,
+ message_count: self.message_count,
+ uid_next: self.uid_next,
+ uid_validity: self.uid_validity,
+ }
+ }
+}
+
+/// [Manager] without [State] - used for error recovery. Get one from [Manager::destruct].
+pub struct ManagerParts {
+ fmm: FolderMailboxMapper,
+ fpm: FolderPathMapper,
+ cf: crate::cf::Manager,
+ buf: Vec<u8>,
+}
+
+impl ManagerParts {
+ /// Return `RecoverMapper` for use in recovering [Session]. See [recover_authenticated].
+ pub fn recover_authenticated<'a>(
+ self,
+ ) -> crate::error::RecoverMapper<'a, Session, Manager, Self> {
+ crate::error::RecoverMapper::new(&|session, mparts| mparts.authenticated(session), self)
+ }
+
+ /// Return `RecoverMapper` for use in recovering [mailbox::Session]. See [recover_selected].
+ pub fn recover_selected<'a>(
+ self,
+ mailbox_name: mailbox::Name,
+ selected_parts: SelectedParts,
+ ) -> crate::error::RecoverMapper<
+ 'a,
+ mailbox::Session,
+ Manager,
+ (Self, mailbox::Name, SelectedParts),
+ > {
+ crate::error::RecoverMapper::new(
+ &|mailbox_session, (mparts, name, selected)| {
+ mparts.selected(name, selected.construct(mailbox_session))
+ },
+ (self, mailbox_name, selected_parts),
+ )
+ }
+
+ /// Return `Manager` in the authenticated state.
+ fn authenticated(self, session: Session) -> Manager {
+ self.construct(State::Authenticated(session))
+ }
+
+ /// Return `Manager` in the selected state.
+ fn selected(self, name: mailbox::Name, selected: mailbox::Selected) -> Manager {
+ self.construct(State::Selected(name, selected))
+ }
+
+ fn construct(self, state: State) -> Manager {
+ Manager {
+ fmm: self.fmm,
+ fpm: self.fpm,
+ cf: self.cf,
+ buf: self.buf,
+ state,
+ }
+ }
+}
+
+/// See [imap] regarding authenticated and selected state.
+#[derive(Debug)]
+pub enum State {
+ Authenticated(Session),
+ // TODO Expand Selected with a little cache mapping message arguments to the result, so we
+ // don't have to hit the server repeatedly when the user runs the same pick over and over, or
+ // something like `show unseen:next` over and over, etc.
+ Selected(mailbox::Name, mailbox::Selected),
+}
+
+/// internal [FolderStore] wrappers
+mod store {
+ use super::*;
+
+ pub fn open(
+ fpm: &FolderPathMapper,
+ folder_name: &folder::Name,
+ uid_validity: UidValidity,
+ ) -> Result<FolderStore, nmh::Error> {
+ FolderStore::open(fpm, folder_name, uid_validity)
+ .map_err(|e| nmh::Error::describe(e, format_args!("open {}", folder_name)))
+ }
+
+ pub fn open_or_create(
+ fpm: &FolderPathMapper,
+ folder_name: &folder::Name,
+ uid_validity: UidValidity,
+ ) -> Result<FolderStore, nmh::Error> {
+ FolderStore::open_or_create(fpm, folder_name, uid_validity)
+ .map_err(|e| nmh::Error::describe(e, format_args!("open {}", folder_name)))
+ }
+}
+
+impl From<nmh::Error> for Error {
+ fn from(e: nmh::Error) -> Self {
+ Self::unrecoverable(e)
+ }
+}
--- /dev/null
+//! Folder manager command implementations live here.
+
+pub mod inc;
+pub mod mark;
+pub mod pick;
--- /dev/null
+//! [inc] implements the `inc` command.
+
+use log::{debug, trace};
+
+use managed_folders as nmf;
+use nmh::folder;
+use nmhimapstore::{FolderPathMapper, FolderStore, ReadTransaction};
+use nmhimaptypes::{uid, Uid};
+
+use crate::imap::{mailbox, FetchResult, Mailbox, MessageResult};
+use crate::manager::{store, Error, Manager, SelectedParts, State};
+use crate::recover_selected;
+
+/// Incorporate new mail and delete messages gone from server.
+/// If `folder` maps to the root of the mailbox hierarchy we synchronize, inc it and all child mailboxes.
+/// Else, inc only the specified mailbox.
+pub fn inc(
+ mut manager: Manager,
+ response: &mut nmf::IncResponse,
+ folder: &folder::Name,
+) -> Result<Manager, Error> {
+ let mailbox: mailbox::Name = manager.fmm.folder_name_to_mailbox(folder)?;
+ trace!(
+ "inc({}) root={} mb={}",
+ folder,
+ manager.fmm.mailbox_root,
+ mailbox
+ );
+ if mailbox.as_str() != manager.fmm.mailbox_root {
+ trace!("inc requested specific mailbox {}", mailbox);
+ manager = inc_mailbox(manager, response, mailbox)?;
+ return Ok(manager);
+ }
+ let mut mailboxes: Vec<Mailbox>;
+ match manager.state {
+ State::Authenticated(session) => {
+ let (value, session) = match session.list(&manager.fmm.mailbox_root) {
+ Err(e) => match e.recover() {
+ Err(e) => return Err(Error::unrecoverable(e)),
+ Ok((session, e)) => {
+ manager.state = State::Authenticated(session);
+ return Err(Error::recoverable(e, manager));
+ }
+ },
+ Ok(v) => v,
+ };
+ manager.state = State::Authenticated(session);
+ mailboxes = value
+ }
+ State::Selected(mailbox_name, mut selected) => {
+ match selected.mailbox_session.list(&manager.fmm.mailbox_root) {
+ Err(recoverable) => match recoverable.recover() {
+ Err(e) => return Err(Error::unrecoverable(e)),
+ Ok((mailbox_session, e)) => {
+ selected.mailbox_session = mailbox_session;
+ manager.state = State::Selected(mailbox_name, selected);
+ return Err(Error::recoverable(e, manager));
+ }
+ },
+ Ok((value, mailbox_session)) => {
+ selected.mailbox_session = mailbox_session;
+ manager.state = State::Selected(mailbox_name, selected);
+ mailboxes = value
+ }
+ }
+ }
+ }
+ mailboxes.sort();
+ for mailbox in mailboxes {
+ if !mailbox.selectable {
+ trace!("inc {} not selectable", mailbox.name);
+ // TODO if this is the mailbox named by `folder`, report error here.
+ continue;
+ }
+ if !manager.cf.rules.included(mailbox.name.as_str()) {
+ trace!("inc {} excluded by user", mailbox.name);
+ continue;
+ }
+ trace!("inc {}", mailbox.name);
+ manager = inc_mailbox(manager, response, mailbox.name)?;
+ }
+ Ok(manager)
+}
+
+fn inc_mailbox(
+ manager: Manager,
+ response: &mut nmf::IncResponse,
+ mailbox_name: mailbox::Name,
+) -> Result<Manager, Error> {
+ let (state, mparts) = manager.destruct();
+ let selected: mailbox::Selected = match state {
+ State::Authenticated(session) => match session.select(&mailbox_name) {
+ Err(recoverable) => match recoverable.recover() {
+ Err(e) => return Err(Error::unrecoverable(e)),
+ Ok((session, e)) => {
+ return Err(Error::recoverable(e, mparts.authenticated(session)));
+ }
+ },
+ Ok(selected) => {
+ trace!("inc SELECT {} from Authenticated", mailbox_name);
+ selected
+ }
+ },
+ State::Selected(selected_name, mut selected) => {
+ if selected_name == mailbox_name {
+ trace!("inc {} already Selected", mailbox_name);
+ selected
+ } else {
+ match selected.mailbox_session.select(&mailbox_name) {
+ Err(recoverable) => match recoverable.recover() {
+ Err(e) => return Err(Error::unrecoverable(e)),
+ Ok((session, e)) => {
+ selected.mailbox_session = session;
+ return Err(Error::recoverable(
+ e,
+ mparts.selected(selected_name, selected),
+ ));
+ }
+ },
+ Ok(selected) => {
+ trace!(
+ "inc SELECT {} from Selected({})",
+ mailbox_name,
+ selected_name
+ );
+ selected
+ }
+ }
+ }
+ }
+ };
+
+ let folder_name: folder::Name = mparts.fmm.mailbox_name_to_folder(mailbox_name.as_str())?;
+ let mut store: FolderStore =
+ store::open_or_create(&mparts.fpm, &folder_name, selected.uid_validity)?;
+
+ // SEARCH all messages into uid_set.
+ let (mailbox_session, selected_parts) = SelectedParts::destruct(selected);
+ let (uid_set, mailbox_name, mut selected, mut mparts) = recover_selected!(
+ mparts,
+ mailbox_name,
+ selected_parts,
+ mailbox_session.search_all()
+ );
+
+ if selected.message_count > 0 {
+ // TODO(rust-lang#71126): (selected, store) = fetch(...
+ let result = fetch(
+ store,
+ response,
+ selected,
+ &mut mparts.buf,
+ &uid_set,
+ &mparts.cf,
+ &mparts.fpm,
+ &folder_name,
+ )?;
+ selected = result.0;
+ store = result.1;
+ }
+ // TODO make deletion optional? maybe have some option like inc -pick-delete that picks the
+ // messages to be deleted so you can scan, show, maybe even copy before deleting?
+ delete(uid_set, store, selected.uid_next).map_err(|e| {
+ nmh::Error::describe(
+ e,
+ format_args!("inc looking for messages to delete in {}", folder_name),
+ )
+ })?;
+ Ok(mparts.selected(mailbox_name, selected))
+}
+
+/// Use [mailbox::Session::fetch] to fetch messages into `store`.
+fn fetch(
+ mut store: FolderStore,
+ response: &mut nmf::IncResponse,
+ mut selected: mailbox::Selected,
+ buf: &mut [u8],
+ uid_set: &uid::Set,
+ cf: &crate::cf::Manager,
+ fpm: &FolderPathMapper,
+ folder_name: &folder::Name,
+) -> Result<(mailbox::Selected, FolderStore), nmh::Error> {
+ let mut appender = store
+ .append(fpm, folder_name)
+ .map_err(|e| nmh::Error::describe(e, format_args!("inc open {}", folder_name)))?;
+ // This is not the same as appender.highest_uid as a
+ // UID greater than that may have been allocated and then deleted before we
+ // got here, and we don't want to waste resources attempting to
+ // FETCH nothing from such a folder on every sync.
+ if let Some(uidnext) = appender.uidnext {
+ if uidnext == selected.uid_next {
+ // covered by test-inc:'inc after inc finds no new mail'
+ debug!("UIDNEXT hasn't changed from {}", uidnext);
+ store = appender.close();
+ return Ok((selected, store));
+ }
+ }
+
+ let highest_uid_or_zero = appender.highest_uid.map(u32::from).unwrap_or(0);
+ let mut uids_to_fetch = uid_set
+ .iter()
+ .skip_while(|uid| **uid <= highest_uid_or_zero);
+
+ let mut result: FetchResult = selected
+ .mailbox_session
+ .fetch(cf.max_fetch_messages_in_flight, &mut uids_to_fetch)?;
+ let mailbox_session: mailbox::Session = loop {
+ match result {
+ FetchResult::Message(uid, seen, mut message_reader) => {
+ trace!("inc receiving uid={} seen={}", uid, seen,);
+ let mut writer = appender.append(uid).map_err(|e| {
+ nmh::Error::describe(e, format_args!("inc append message to {}", folder_name))
+ })?;
+ result = loop {
+ match message_reader.read(buf) {
+ Err(e) => {
+ return Err(e);
+ }
+ Ok(MessageResult::Reading(bytes, new_reader)) => {
+ writer.write_all(bytes).map_err(|e| {
+ nmh::Error::describe(
+ e,
+ format_args!("inc write to message file in {}", folder_name),
+ )
+ })?;
+ message_reader = new_reader;
+ }
+ Ok(MessageResult::Done(r)) => break r,
+ }
+ };
+ let (msgnum, new_appender) = writer.close().map_err(|e| {
+ nmh::Error::describe(
+ e,
+ format_args!("inc close message file in {}", folder_name),
+ )
+ })?;
+ appender = new_appender;
+ response
+ .message(folder_name, msgnum, !seen)
+ .map_err(|e| nmh::Error::new(e.to_string()))?;
+ }
+ FetchResult::Done(session) => break session,
+ }
+ };
+ // TODO Don't write uidnext unless it changed.
+ store = appender.close();
+ let uidnext = selected.uid_next;
+ store.write_uidnext(uidnext).map_err(|e| {
+ nmh::Error::describe(
+ e,
+ format_args!("inc store UIDNEXT={} in {}", uidnext, folder_name),
+ )
+ })?;
+ selected.mailbox_session = mailbox_session;
+ Ok((selected, store))
+}
+
+/// Delete `uid_set` from `store`, as they are gone from the server.
+fn delete(uid_set: uid::Set, store: FolderStore, uidnext: Uid) -> Result<(), nmh::Error> {
+ // TODO write_transaction and add a delete_message to it?
+ let txn = store.read_transaction()?;
+ let uid2msg_iter = txn.uid_to_message_number_cursor()?;
+ for result in uid2msg_iter {
+ let (uid, msgnum) = result?;
+ let uid_u32: u32 = uid.into();
+ if uid_u32 >= uidnext.into() {
+ break;
+ }
+ if uid_set.contains(uid_u32) {
+ trace!("inc NOT deleting msg={} uid={}", msgnum, uid);
+ } else {
+ trace!("inc deleting msg={} uid={}", msgnum, uid);
+ // TODO add WriteTransaction::delete ?
+ store.delete_message(msgnum, uid)?;
+ }
+ }
+ Ok(())
+}
--- /dev/null
+//! [add::add] implements `mark -add` and [delete::delete] `mark -del`
+
+use managed_folders as nmf;
+use nmf::command;
+use nmh::{folder, message};
+
+use crate::imap::{mailbox, Flag};
+use crate::manager::pick::{
+ check_for_all_or_unseen, pick_messages, write_sequences, LocalPicker, RemotePicker,
+};
+use crate::manager::{Error, Manager, ManagerParts, SelectedParts, State};
+use crate::{recover_authenticated, recover_selected};
+
+mod add;
+pub use add::add;
+
+mod delete;
+pub use delete::delete;
+
+// TODO Read Unseen-Sequence.
+
+fn mark<MarkServer, MarkLocal>(
+ mark_server: MarkServer,
+ mark_local: MarkLocal,
+ manager: Manager,
+ mut command: command::Mark,
+) -> Result<Manager, Error>
+where
+ MarkServer:
+ Fn(mailbox::Session, &str, &[Flag]) -> Result<((), mailbox::Session), mailbox::Error>,
+ MarkLocal: Fn(
+ std::slice::Iter<(&str, &str)>,
+ message::Set,
+ message::Set,
+ Vec<&str>,
+ ) -> Result<Vec<(String, String)>, nmh::Error>,
+{
+ // TODO Support other sequences; map them to flags? Optionally?
+ let need_unseen = check_for_all_or_unseen(&mut command.msgs);
+ if need_unseen || command.sequences.contains(&"unseen") {
+ mark_with_unseen(
+ mark_server,
+ mark_local,
+ manager,
+ command.folder,
+ command.msgs,
+ command.sequences,
+ )
+ } else {
+ Ok(mark_without_unseen(
+ mark_local,
+ manager,
+ command.folder,
+ command.msgs,
+ command.sequences,
+ )?)
+ }
+}
+
+fn mark_with_unseen<MarkServer, MarkLocal>(
+ mark_server: MarkServer,
+ mark_local: MarkLocal,
+ mut manager: Manager,
+ folder_name: folder::Name,
+ specs: Vec<&str>,
+ mark_sequences: Vec<&str>,
+) -> Result<Manager, Error>
+where
+ MarkServer:
+ Fn(mailbox::Session, &str, &[Flag]) -> Result<((), mailbox::Session), mailbox::Error>,
+ MarkLocal: Fn(
+ std::slice::Iter<(&str, &str)>,
+ message::Set,
+ message::Set,
+ Vec<&str>,
+ ) -> Result<Vec<(String, String)>, nmh::Error>,
+{
+ let (state, mparts) = manager.destruct();
+ let session = match state {
+ State::Authenticated(session) => session,
+ State::Selected(_, selected) => match selected.mailbox_session.close() {
+ Err(e) => return Err(Error::unrecoverable(nmh::Error::wrap(e))),
+ Ok(session) => {
+ log::trace!("rmm CLOSEd into Authenticated state for STORE (TODO check first)");
+ session
+ }
+ },
+ };
+
+ let mailbox_name: mailbox::Name = mparts.fmm.folder_name_to_mailbox(&folder_name)?;
+ let (selected, mparts): (mailbox::Selected, ManagerParts) =
+ recover_authenticated!(mparts, session.select(&mailbox_name));
+
+ let picker = RemotePicker::new(&mparts.fpm, selected.uid_validity, &folder_name)?;
+
+ // SEARCH UNSEEN messages into unseen_uids.
+ let (mailbox_session, selected_parts) = SelectedParts::destruct(selected);
+ let (unseen_uids, mailbox_name, selected, mparts) = recover_selected!(
+ mparts,
+ mailbox_name,
+ selected_parts,
+ mailbox_session.search_unseen()
+ );
+
+ let picker_txn = picker.txn()?;
+ let mut picked_messages = message::Set::new();
+ let mut unseen_messages: message::Set = picker_txn.to_message_numbers(unseen_uids)?;
+ let mut uids_to_add = Vec::<nmhimaptypes::Uid>::new();
+ let sequences = picker.parse_sequences()?;
+ pick_messages(
+ specs,
+ &sequences,
+ // TODO Rather than clone it, move our unseen_messages in and then take it back out later?
+ unseen_messages.clone(),
+ picker_txn.cursor()?,
+ |(msgnum, uid)| {
+ let msgnum_i32: i32 = msgnum.into();
+ picked_messages.insert(msgnum_i32);
+ // TODO move this check out
+ if mark_sequences.contains(&"unseen") {
+ unseen_messages.remove(msgnum_i32);
+ uids_to_add.push(uid);
+ }
+ },
+ )?;
+
+ if uids_to_add.is_empty() {
+ manager = mparts.selected(mailbox_name, selected);
+ } else {
+ // TODO Should this be a UidSet, and this below its Display impl?
+ // TODO use UID ranges
+ let s: String = uids_to_add
+ .iter()
+ .map(|u| u.to_string())
+ .collect::<Vec<String>>()
+ .join(",");
+ let (mailbox_session, selected_parts) = SelectedParts::destruct(selected);
+ let (_, _, selected, mparts) = recover_selected!(
+ mparts,
+ mailbox_name,
+ selected_parts,
+ mark_server(mailbox_session, &s, &[Flag::Seen])
+ );
+ // CLOSE to persist the flag changes.
+ manager = mparts.authenticated(selected.mailbox_session.close().map_err(nmh::Error::wrap)?);
+ }
+
+ write_sequences(
+ picker.seq_path(),
+ mark_local(
+ sequences.iter(),
+ picked_messages,
+ unseen_messages,
+ mark_sequences,
+ )?,
+ )?;
+ Ok(manager)
+}
+
+fn mark_without_unseen<F>(
+ mark_sequences: F,
+ manager: Manager,
+ folder_name: folder::Name,
+ specs: Vec<&str>,
+ sequences_to_mark: Vec<&str>,
+) -> Result<Manager, nmh::Error>
+where
+ F: Fn(
+ std::slice::Iter<(&str, &str)>,
+ message::Set,
+ message::Set,
+ Vec<&str>,
+ ) -> Result<Vec<(String, String)>, nmh::Error>,
+{
+ let picker = LocalPicker::new(&manager.fpm, &folder_name)?;
+ let sequences: nmf::Components = picker.parse_sequences()?;
+
+ let mut picked_messages = message::Set::new();
+ pick_messages(
+ specs,
+ &sequences,
+ message::Set::new(),
+ picker.txn()?.cursor()?,
+ |(msgnum, _)| {
+ picked_messages.insert(i32::from(msgnum));
+ },
+ )?;
+
+ write_sequences(
+ picker.seq_path(),
+ mark_sequences(
+ sequences.iter(),
+ picked_messages,
+ message::Set::new(),
+ sequences_to_mark,
+ )?,
+ )?;
+
+ Ok(manager)
+}
--- /dev/null
+//! Implements `mark -add`.
+
+use crate::manager::pick::parse_msgs;
+use crate::manager::{Error, Manager};
+
+pub fn add(manager: Manager, command: managed_folders::command::Mark) -> Result<Manager, Error> {
+ super::mark(
+ |mailbox_session, s, flags| mailbox_session.del_flags(s, flags),
+ update_sequences_adding_marked,
+ manager,
+ command,
+ )
+}
+
+fn update_sequences_adding_marked(
+ sequences: std::slice::Iter<(&str, &str)>,
+ picked_messages: nmh::message::Set,
+ unseen_messages: nmh::message::Set,
+ mut mark_sequences: Vec<&str>,
+) -> Result<Vec<(String, String)>, nmh::Error> {
+ // TODO make nmh-picked configurable. if user already configured Previous-Sequence, use that?
+ let mut result = vec![("nmh-picked".to_string(), picked_messages.to_string())];
+ if !unseen_messages.is_empty() {
+ result.push(("unseen".to_string(), unseen_messages.to_string()));
+ }
+ for (name, value) in sequences {
+ if *name == "nmh-picked" || *name == "unseen" {
+ continue;
+ }
+ let mut marked = None;
+ for i in 0..mark_sequences.len() {
+ if mark_sequences[i] == *name {
+ marked = Some(i);
+ let mut ranges = parse_msgs(value)?;
+ ranges.append(&mut picked_messages.clone());
+ if !ranges.is_empty() {
+ result.push((name.to_string(), ranges.to_string()));
+ }
+ break;
+ }
+ }
+ match marked {
+ None => result.push((name.to_string(), value.to_string())),
+ Some(i) => {
+ mark_sequences.remove(i);
+ }
+ }
+ }
+ for new_sequence in mark_sequences.iter() {
+ result.push((new_sequence.to_string(), picked_messages.to_string()))
+ }
+ Ok(result)
+}
--- /dev/null
+//! Implements `mark -del`.
+
+use crate::manager::pick::parse_msgs;
+use crate::manager::{Error, Manager};
+
+pub fn delete(manager: Manager, command: managed_folders::command::Mark) -> Result<Manager, Error> {
+ super::mark(
+ |mailbox_session, s, flags| mailbox_session.add_flags(s, flags),
+ update_sequences_deleting_marked,
+ manager,
+ command,
+ )
+}
+
+fn update_sequences_deleting_marked(
+ sequences: std::slice::Iter<(&str, &str)>,
+ picked_messages: nmh::message::Set,
+ unseen_messages: nmh::message::Set,
+ mark_sequences: Vec<&str>,
+) -> Result<Vec<(String, String)>, nmh::Error> {
+ // TODO make nmh-picked configurable. if user already configured Previous-Sequence, use that?
+ let mut result = vec![("nmh-picked".to_string(), picked_messages.to_string())];
+ if !unseen_messages.is_empty() {
+ result.push(("unseen".to_string(), unseen_messages.to_string()));
+ }
+ for (name, value) in sequences {
+ if *name == "nmh-picked" || *name == "unseen" {
+ continue;
+ }
+ let mut marked = false;
+ for mark_seq in &mark_sequences {
+ if name == mark_seq {
+ let mut ranges = parse_msgs(value)?;
+ ranges = ranges.clone_without(&picked_messages);
+ if !ranges.is_empty() {
+ result.push((name.to_string(), ranges.to_string()));
+ }
+ marked = true;
+ break;
+ }
+ }
+ if !marked {
+ result.push((name.to_string(), value.to_string()));
+ }
+ }
+ Ok(result)
+}
--- /dev/null
+//! [pick] implements the message picking.
+
+use managed_folders as nmf;
+use nmf::command;
+use nmh::{folder, message};
+
+use crate::imap::mailbox;
+use crate::manager::pick::{
+ check_for_all_or_unseen, pick_messages, write_sequences, LocalPicker, RemotePicker,
+};
+use crate::manager::{Error, Manager, ManagerParts, SelectedParts, State};
+use crate::{recover_authenticated, recover_selected};
+
+// TODO Read Unseen-Sequence.
+
+pub fn pick(manager: Manager, mut command: command::Pick) -> Result<Manager, Error> {
+ let need_unseen = check_for_all_or_unseen(&mut command.msgs);
+ if need_unseen || command.need_unseen {
+ pick_with_unseen(manager, command.folder, command.msgs)
+ } else {
+ Ok(pick_without_unseen(manager, command.folder, command.msgs)?)
+ }
+}
+
+fn pick_without_unseen(
+ manager: Manager,
+ folder_name: folder::Name,
+ specs: Vec<&str>,
+) -> Result<Manager, nmh::Error> {
+ let picker = LocalPicker::new(&manager.fpm, &folder_name)?;
+ let sequences: nmf::Components = picker.parse_sequences()?;
+
+ let mut picked_messages = message::Set::new();
+ pick_messages(
+ specs,
+ &sequences,
+ message::Set::new(),
+ picker.txn()?.cursor()?,
+ |(msgnum, _)| {
+ picked_messages.insert(i32::from(msgnum));
+ },
+ )?;
+
+ write_sequences(
+ picker.seq_path(),
+ update_sequences(sequences.iter(), picked_messages, message::Set::new()),
+ )?;
+
+ Ok(manager)
+}
+
+// TODO dedup with mark_with_unseen
+fn pick_with_unseen(
+ mut manager: Manager,
+ folder_name: folder::Name,
+ specs: Vec<&str>,
+) -> Result<Manager, Error> {
+ // TODO Support other sequences; map them to flags? Optionally?
+ // 0. SEARCH UNSEEN if need_unseen
+ let (state, mparts) = manager.destruct();
+ let session = match state {
+ State::Authenticated(session) => session,
+ State::Selected(_, selected) => match selected.mailbox_session.close() {
+ Err(e) => return Err(Error::unrecoverable(nmh::Error::wrap(e))),
+ Ok(session) => {
+ log::trace!("rmm CLOSEd into Authenticated state for SEARCH (TODO check first)");
+ session
+ }
+ },
+ };
+
+ let mailbox_name: mailbox::Name = mparts.fmm.folder_name_to_mailbox(&folder_name)?;
+ let (selected, mparts): (mailbox::Selected, ManagerParts) =
+ recover_authenticated!(mparts, session.select(&mailbox_name));
+
+ let picker = RemotePicker::new(&mparts.fpm, selected.uid_validity, &folder_name)?;
+
+ // SEARCH UNSEEN messages into unseen_uids.
+ let (mailbox_session, selected_parts) = SelectedParts::destruct(selected);
+ let (unseen_uids, _, selected, mparts) = recover_selected!(
+ mparts,
+ mailbox_name,
+ selected_parts,
+ mailbox_session.search_unseen()
+ );
+
+ let mut picked_messages = message::Set::new();
+ let sequences = picker.parse_sequences()?;
+ let picker_txn = picker.txn()?;
+ let unseen_messages: message::Set = picker_txn.to_message_numbers(unseen_uids)?;
+ pick_messages(
+ specs,
+ &sequences,
+ // TODO Rather than clone it, move our unseen_messages in and then take it back out later?
+ unseen_messages.clone(),
+ picker_txn.cursor()?,
+ |(msgnum, _)| {
+ picked_messages.insert(i32::from(msgnum));
+ },
+ )?;
+
+ // CLOSE to persist the flag changes.
+ manager = mparts.authenticated(selected.mailbox_session.close().map_err(nmh::Error::wrap)?);
+
+ write_sequences(
+ picker.seq_path(),
+ update_sequences(sequences.iter(), picked_messages, unseen_messages),
+ )?;
+
+ Ok(manager)
+}
+
+fn update_sequences<'a>(
+ sequences: std::slice::Iter<(&'a str, &'a str)>,
+ picked_messages: message::Set,
+ unseen_messages: message::Set,
+) -> Vec<(String, String)> {
+ // TODO make nmh-picked configurable. if user already configured Previous-Sequence, use that?
+ let mut result = vec![("nmh-picked".to_string(), picked_messages.to_string())];
+ if !unseen_messages.is_empty() {
+ result.push(("unseen".to_string(), unseen_messages.to_string()));
+ }
+ for (name, value) in sequences {
+ if *name == "nmh-picked" || *name == "unseen" {
+ continue;
+ }
+ result.push((name.to_string(), value.to_string()));
+ }
+ result
+}
--- /dev/null
+//! [FolderMailboxMapper] maps between folder paths and mailbox names.
+
+// TODO This assumes mailbox hierarchy separator is '/' which is not true.
+// We have to check the LIST "" % response to find it.
+// This is also just messy. What do we really need?
+#[derive(Debug)]
+pub struct FolderMailboxMapper {
+ name: String,
+ // TODO
+ pub mailbox_root: String,
+}
+
+impl FolderMailboxMapper {
+ pub fn new(name: String, mailbox_root: String) -> Self {
+ Self { name, mailbox_root }
+ }
+
+ pub fn folder_name_to_mailbox(
+ &self,
+ folder: &nmh::folder::Name,
+ ) -> Result<crate::imap::mailbox::Name, nmh::Error> {
+ let stripped = match folder.as_path().strip_prefix(&self.name) {
+ Ok(value) => value,
+ Err(e) => todo!("handle wrong folder {}", e),
+ };
+ match stripped.to_str() {
+ Some(value) => Ok(crate::imap::mailbox::Name::from(format!(
+ "{}{}",
+ self.mailbox_root, value
+ ))),
+ None => todo!("mailbox name conversion"),
+ }
+ }
+
+ pub fn mailbox_name_to_folder(&self, mailbox: &str) -> Result<nmh::folder::Name, nmh::Error> {
+ nmh::folder::Name::from_utf8(
+ std::path::Path::new(&self.name)
+ .join(mailbox.strip_prefix(&self.mailbox_root).unwrap_or(mailbox)),
+ )
+ }
+}
--- /dev/null
+//! Local and remote message picking
+//!
+//! The names here could use some work but hopefully it all makes sense. Various commands take
+//! "message specifications" which describe the set of messages on which to operate. See the
+//! `mh-sequence(5)` man page for details.
+//!
+//! Most of these specifications can be resolved locally. Only one case requires contacting the
+//! server: resolving the unseen sequence.
+//!
+//! To avoid hitting the server unnecessarily we have [LocalPicker] for local-only message
+//! resolution, and [RemotePicker] for local and remote (maybe we should call it
+//! `LocalAndRemotePicker` or something).
+//!
+//! To start with, use [check_for_all_or_unseen] to determine whether assistance from the server is
+//! required. This function also helps special-case "all" and "first-last" specifications. If,
+//! after passing a [Vec] to [check_for_all_or_unseen], element 0 is "all", the caller knows the
+//! operation will be purely local.
+//!
+//! Next, use [LocalPicker] or [RemotePicker] with [pick_messages] to pick local messages.
+//! With [RemotePicker], the caller also gets the [Uid]s.
+//!
+//! [parse_msgs] and [write_sequences] are used to update sequences files.
+
+use std::num::{NonZeroI32, NonZeroU32};
+
+use managed_folders as nmf;
+use nmh::{folder, message};
+use nmhimapstore::{
+ FolderPathMapper, FolderStore, MessageCursor, MessageNumber2UidCursor, MessageNumberCursor,
+ MessageNumberTransaction, MessageNumberView, ReadOnlyTransaction, ReadTransaction,
+};
+use nmhimaptypes::{uid, Uid, UidValidity};
+
+use crate::manager::store;
+use nmh::Error;
+
+// TODO Unseen-Sequence
+/// Set `msgs[0]` to `"all"` (leaving all other elements untouched) if any element is "all" or
+/// "first-last" and return true if the Unseen-Sequence is referenced.
+pub fn check_for_all_or_unseen(msgs: &mut Vec<&str>) -> bool {
+ let mut need_unseen = false;
+ for i in 0..msgs.len() {
+ let spec = msgs[i];
+ if spec == "all" || spec == "first-last" {
+ msgs[0] = "all";
+ return false;
+ }
+ if spec == "unseen" {
+ need_unseen = true;
+ }
+ }
+ need_unseen
+}
+
+pub struct LocalPicker {
+ msg_only: MessageNumberView,
+ seq_path: std::path::PathBuf,
+ seq_contents: String,
+}
+
+impl LocalPicker {
+ pub fn new(fpm: &FolderPathMapper, folder_name: &folder::Name) -> Result<Self, Error> {
+ let seq_path = fpm.seq_path(folder_name);
+ // TODO locking
+ let seq_contents: String = file_contents_or_empty(&seq_path)?;
+ Ok(Self {
+ msg_only: MessageNumberView::open(fpm, folder_name)
+ .map_err(|e| Error::describe(e, format_args!("{}", folder_name)))?,
+ seq_path,
+ seq_contents,
+ })
+ }
+
+ pub fn parse_sequences(&self) -> Result<nmf::Components, Error> {
+ nmf::parse_components(&self.seq_contents)
+ .map_err(|e| Error::describe(e, format_args!("{}", self.seq_path.display())))
+ }
+
+ pub fn seq_path(&self) -> &std::path::Path {
+ &self.seq_path
+ }
+
+ pub fn txn(&self) -> Result<LocalPickerTransaction, Error> {
+ Ok(LocalPickerTransaction {
+ txn: self.msg_only.read_transaction()?,
+ })
+ }
+}
+
+pub struct LocalPickerTransaction<'a> {
+ txn: MessageNumberTransaction<'a>,
+}
+
+impl<'a> LocalPickerTransaction<'a> {
+ pub fn cursor(&self) -> Result<MessageNumberCursor, Error> {
+ self.txn.cursor()
+ }
+}
+
+pub struct RemotePicker {
+ store: FolderStore,
+ seq_path: std::path::PathBuf,
+ seq_contents: String,
+}
+
+impl RemotePicker {
+ pub fn new(
+ fpm: &FolderPathMapper,
+ uid_validity: UidValidity,
+ folder_name: &folder::Name,
+ ) -> Result<Self, Error> {
+ let seq_path = fpm.seq_path(folder_name);
+ // TODO locking
+ let seq_contents: String = file_contents_or_empty(&seq_path)?;
+ Ok(Self {
+ store: store::open(fpm, folder_name, uid_validity)?,
+ seq_path,
+ seq_contents,
+ })
+ }
+
+ pub fn parse_sequences(&self) -> Result<nmf::Components, Error> {
+ nmf::parse_components(&self.seq_contents)
+ .map_err(|e| Error::describe(e, format_args!("{}", self.seq_path.display())))
+ }
+
+ pub fn seq_path(&self) -> &std::path::Path {
+ &self.seq_path
+ }
+
+ pub fn txn(&self) -> Result<RemotePickerTransaction, Error> {
+ Ok(RemotePickerTransaction {
+ txn: self.store.read_transaction()?,
+ })
+ }
+}
+
+pub struct RemotePickerTransaction<'a> {
+ txn: ReadOnlyTransaction<'a>,
+}
+
+impl<'a> RemotePickerTransaction<'a> {
+ pub fn to_message_numbers(&self, uids: uid::Set) -> Result<message::Set, Error> {
+ let mut msgnums = message::Set::new();
+ for uid in uids.iter() {
+ // TODO is this faster or would it be faster to iterate
+ // lowest-highest unseen UID and pick what matches?
+ msgnums.insert(
+ self.txn
+ .lookup_message_number(Uid::from(
+ NonZeroU32::new(*uid)
+ .ok_or_else(|| Error::new(format!("Uid <= 0: {}", uid)))?,
+ ))?
+ .ok_or_else(|| {
+ // TODO Add test for this and for the fix, don't even do the db lookup.
+ // UID increases monotonically, so any UID not appearing in local db
+ // MUST be greater than the highest UID we've seen before. We can check
+ // that and skip the lookup.
+ todo!("UID {} in search results we haven't inc'd yet", uid)
+ })
+ .unwrap()
+ .into(),
+ );
+ }
+ Ok(msgnums)
+ }
+
+ pub fn cursor(&self) -> Result<MessageNumber2UidCursor, Error> {
+ self.txn.message_number_to_uid_cursor()
+ }
+}
+
+pub fn parse_msgs(msgs: &str) -> Result<message::Set, Error> {
+ let mut ranges = message::Set::new();
+ for spec in msgs.split_ascii_whitespace() {
+ let pieces: Vec<&str> = spec.split('-').collect();
+ if pieces.len() == 1 {
+ // no '-'
+ match pieces[0].parse() {
+ Err(_) => todo!(
+ "check for cur etc. else return error 'bad message list {}'",
+ spec
+ ),
+ Ok(msgnum) => {
+ ranges.insert(msgnum);
+ }
+ }
+ } else if pieces.len() == 2 {
+ // one '-'
+ let beg: i32 = match pieces[0].parse() {
+ Err(_) => todo!(
+ "check for cur etc. else return error 'bad message list {}'",
+ spec
+ ),
+ Ok(value) => value,
+ };
+ let end: i32 = match pieces[1].parse() {
+ Err(_) => todo!(
+ "check for cur etc. else return error 'bad message list {}'",
+ spec
+ ),
+ Ok(value) => value,
+ };
+ for msgnum in beg..end + 1 {
+ ranges.insert(msgnum);
+ }
+ } else {
+ todo!("you can't have two dashes in here")
+ }
+ }
+ Ok(ranges)
+}
+
+/// Call `func` with picked messages.
+///
+/// `specs` is a list of specifications according to `mh-sequenc(5)`.
+/// Any references to sequences therein are resolved in `sequences`, or
+/// `unseen_messages` for the unseen sequence.
+///
+/// If `cursor` is a [MessageNumber2UidCursor], `func` also receives corresponding UIDs.
+pub fn pick_messages<T, C: MessageCursor<T>, F: FnMut((message::Number, T))>(
+ specs: Vec<&str>,
+ sequences: &nmf::Components,
+ unseen_messages: message::Set,
+ cursor: C,
+ mut func: F,
+) -> Result<(), Error> {
+ let ranges_to_pick: message::Set = specs_to_ranges(specs, sequences, unseen_messages)?;
+ if let Some((beg, end)) = ranges_to_pick.ends() {
+ let mut item: Option<(message::Number, T)> = cursor
+ .get(message::Number::from(NonZeroI32::new(beg).ok_or_else(
+ || Error::new(format!("message number <= 0: {}", beg)),
+ )?))?;
+ loop {
+ match item {
+ None => return Ok(()),
+ Some(pair) => {
+ let msgnum: i32 = pair.0.into();
+ if msgnum > end {
+ break;
+ }
+ if ranges_to_pick.contains(msgnum) {
+ func(pair);
+ }
+ }
+ }
+ item = cursor.next()?;
+ }
+ }
+ Ok(())
+}
+
+pub fn write_sequences(
+ seq_path: &std::path::Path,
+ sequences: Vec<(String, String)>,
+) -> Result<(), Error> {
+ write_sequences_unwrapped(seq_path, sequences)
+ .map_err(|e| Error::describe(e, format_args!("write sequences to {}", seq_path.display())))
+}
+
+fn write_sequences_unwrapped(
+ seq_path: &std::path::Path,
+ sequences: Vec<(String, String)>,
+) -> std::io::Result<()> {
+ // TODO locking
+ let mut file = std::fs::File::create(seq_path)?;
+ use std::io::Write;
+ for (name, value) in sequences {
+ file.write_all(format!("{}: {}\n", name, value).as_bytes())?
+ }
+ file.sync_all()
+}
+
+// TODO not a great home for this
+fn file_contents_or_empty(path: &std::path::Path) -> Result<String, Error> {
+ match std::fs::read_to_string(&path) {
+ Err(e) => match e.kind() {
+ std::io::ErrorKind::NotFound => Ok("".to_owned()),
+ _ => Err(Error::describe(e, format_args!("read {}", path.display()))),
+ },
+ Ok(contents) => Ok(contents),
+ }
+}
+
+/// Return one [message::Set] from the user-provided vector of message specifications.
+/// If any of the specifications reference sequences, look them up in `sequences`, except for the
+/// Unseen-Sequence, for which `unseen_messages` is used.
+fn specs_to_ranges(
+ specs: Vec<&str>,
+ sequences: &nmf::Components,
+ unseen_messages: message::Set,
+) -> Result<message::Set, Error> {
+ let mut unseen_messages = Some(unseen_messages);
+ let mut ranges_to_pick = message::Set::new();
+ for spec in &specs {
+ if *spec == "all" {
+ ranges_to_pick = message::Set::all();
+ break;
+ }
+ if spec.contains('=') || spec.contains(':') {
+ todo!("= and : not yet supported")
+ }
+ // may be '1', '1-2', or 'seq'
+ // TODO while sequences cannot occur in ranges, special words like 'first', 'cur', 'prev', 'next' can
+ let expanded_specs: &str = if spec.contains('-') {
+ spec
+ } else {
+ match spec.parse::<u32>() {
+ Err(_) => {
+ if *spec == "unseen" {
+ if let Some(mut taken) = unseen_messages.take() {
+ ranges_to_pick.append(&mut taken)
+ }
+ // TODO this is poorly structured
+ ""
+ } else {
+ sequences.get(spec).unwrap_or(
+ // TODO no such sequence; return "bad message list {spec}"?
+ "",
+ )
+ }
+ }
+ Ok(_) => spec,
+ }
+ };
+ ranges_to_pick.append(&mut parse_msgs(expanded_specs)?);
+ }
+ Ok(ranges_to_pick)
+}
--- /dev/null
+//! This is a bare-bones regex.h wrapper. It simply panics if anything goes
+//! wrong. At some point we should polish this.
+
+use std::ffi::CStr;
+use std::mem::MaybeUninit;
+use std::os::raw::{c_char, c_int};
+
+use libc::{regcomp, regex_t, regexec, regfree, regmatch_t, REG_NOMATCH, REG_NOSUB};
+
+#[derive(Debug)]
+pub struct Regex(MaybeUninit<regex_t>);
+
+impl Regex {
+ pub fn compile(pattern: &CStr) -> Self {
+ let mut this = Self(MaybeUninit::uninit());
+ let preg: *mut regex_t = this.0.as_mut_ptr();
+ let pat_cstr: *const c_char = pattern.as_ptr();
+ let cflags: c_int = REG_NOSUB;
+ if unsafe { regcomp(preg, pat_cstr, cflags) } != 0 {
+ todo!("regcomp error")
+ }
+ this
+ }
+
+ pub fn matches(&self, s: &str) -> bool {
+ let cstring = match std::ffi::CString::new(s) {
+ // This only returns error on embedded null byte and it's fair to call that not a match.
+ Err(_) => return false,
+ Ok(value) => value,
+ };
+ let preg: *const regex_t = self.0.as_ptr();
+ let string: *const c_char = cstring.as_c_str().as_ptr();
+ let nmatch = 0;
+ let pmatch: *mut regmatch_t = std::ptr::null_mut();
+ let eflags = 0;
+ match unsafe { regexec(preg, string, nmatch, pmatch, eflags) } {
+ 0 => true,
+ REG_NOMATCH => false,
+ _ => todo!("regexec error"),
+ }
+ }
+}
+
+impl Drop for Regex {
+ fn drop(&mut self) {
+ let preg: *mut regex_t = self.0.as_mut_ptr();
+ unsafe { regfree(preg) }
+ }
+}
+
+#[cfg(test)]
+mod tests;
--- /dev/null
+use std::ffi::CString;
+
+use super::*;
+
+#[test]
+fn matched() {
+ let re = Regex::compile(CString::new(".").unwrap().as_c_str());
+ assert!(re.matches("."));
+}
+
+#[test]
+fn not() {
+ let re = Regex::compile(CString::new("nah").unwrap().as_c_str());
+ assert!(!re.matches("."));
+}
--- /dev/null
+use managed_folders as nmf;
+
+use crate::cf;
+use crate::imap;
+use crate::logging;
+use crate::manager::Manager;
+
+pub fn configure_and_run(context: &nmh::Context, name: String) -> Result<(), String> {
+ let profile = context.profile();
+ let prefix = format!("{}-", name);
+ let prefixed = profile.prefixed(&prefix);
+ let mut message_buffer = vec![0; 8192];
+ match configure(&mut message_buffer, context, &profile, name, &prefixed)? {
+ None => Ok(()),
+ Some((listener, manager)) => run(listener, &mut message_buffer, manager),
+ }
+}
+
+fn configure(
+ message_buffer: &mut [u8],
+ context: &nmh::Context,
+ profile: &nmh::Profile,
+ name: String,
+ prefixed: &nmh::profile::Prefixed,
+) -> Result<Option<(std::os::unix::net::UnixListener, Manager)>, String> {
+ // Install logging as soon as possible.
+ logging::install(prefixed)?;
+
+ // Fully process configuration before attempting any work.
+ let init = cf::Init::new(prefixed)?;
+ let cf = cf::Manager::new(prefixed)?;
+
+ std::fs::create_dir_all(&cf.db_path)
+ .map_err(|e| format!("{}: {}", cf.db_path.display(), e))?;
+
+ // As soon as possible, we bind the listening socket and tell the calling nmh we are ready.
+ let listener = std::os::unix::net::UnixListener::bind(init.socket_path)
+ .map_err(|e| format!("{}: {}", init.socket_path.display(), e))?;
+ // Tell caller that we are ready.
+ if unsafe {
+ let lf = '\n';
+ libc::write(1, &lf as *const char as *const libc::c_void, 1)
+ } < 0
+ {
+ return Err(format!(
+ "write(1, \"\\n\", 1): {}",
+ std::io::Error::last_os_error()
+ ));
+ }
+ if unsafe { libc::close(1) } < 0 {
+ return Err(format!("close(1): {}", std::io::Error::last_os_error()));
+ }
+
+ let (stream, _sockaddr) = listener.accept().map_err(log_and_convert)?;
+
+ // Now that we're listening, we have a place to send errors, so setup the stderr log.
+ logging::redirect_stderr_to(cf.db_path.join("stderr"))?;
+
+ let mail_path: &std::path::Path = profile.path()?;
+ match make_manager(context, name, cf, init, mail_path) {
+ Err(e) => {
+ nmf::close_with_error(stream, &e).map_err(log_and_convert)?;
+ Err(e)
+ }
+ Ok(manager) => match handle(message_buffer, stream, manager)? {
+ None => Ok(None),
+ Some(manager) => Ok(Some((listener, manager))),
+ },
+ }
+}
+
+fn make_manager(
+ context: &nmh::Context,
+ name: String,
+ cf: cf::Manager,
+ init: cf::Init,
+ mail_path: &std::path::Path,
+) -> Result<Manager, String> {
+ Ok(Manager::new(
+ crate::manager::FolderMailboxMapper::new(name, cf.mailbox_root.to_string()),
+ nmhimapstore::FolderPathMapper::new(mail_path.to_path_buf(), cf.db_path.to_path_buf()),
+ imap::auth::login(context, init.auth).map_err(log_and_convert)?,
+ cf,
+ ))
+}
+
+fn run(
+ listener: std::os::unix::net::UnixListener,
+ message_buffer: &mut [u8],
+ mut manager: Manager,
+) -> Result<(), String> {
+ loop {
+ let (stream, _sockaddr) = listener.accept().map_err(log_and_convert)?;
+ match handle(message_buffer, stream, manager)? {
+ None => return Ok(()),
+ Some(m) => manager = m,
+ }
+ // TODO actually i'm pretty sure that hang was my bug which i fixed
+ // TODO m_getfld2 hangs :(
+ // temporary hack is to reconnect for every command
+ }
+}
+
+fn handle(
+ message_buffer: &mut [u8],
+ stream: std::os::unix::net::UnixStream,
+ manager: Manager,
+) -> Result<Option<Manager>, String> {
+ let reader = nmf::FolderManagerProtocol::new(stream);
+ let (message, reader): (nmf::Message, nmf::FolderManagerProtocol) = reader
+ .read_message(message_buffer)
+ .map_err(|e| e.to_string())?;
+ let command = reader.read_command(&message).map_err(|e| e.to_string())?;
+ Ok(match command {
+ nmf::Command::Quit => None,
+ _ => {
+ let (new_manager, reader) = manager.run(command)?;
+ reader.close().map_err(log_and_convert)?;
+ Some(new_manager)
+ }
+ })
+}
+
+fn log_and_convert<T: std::error::Error>(e: T) -> String {
+ logging::error(&e);
+ e.to_string()
+}
--- /dev/null
+[package]
+name = "mhimap"
+# TODO derive from nmh version
+version = "0.0.0"
+edition = "2018"
+
+[dependencies]
+nmh = { path = "../../sbr/rust" }
+nmhimapstore = { path = "../store" }
--- /dev/null
+use std::io::{Read, Write};
+use std::os::unix::net::UnixStream;
+use std::path::{Path, PathBuf};
+
+fn main() -> std::io::Result<()> {
+ // TODO mh-style switches
+ let mut args = std::env::args();
+ let (context, name): (nmh::Context, String) = parse_args(&mut args);
+ let profile: nmh::Profile = context.profile();
+ let prefix = format!("{}-", name);
+ let prefixed: nmh::profile::Prefixed = profile.prefixed(&prefix);
+
+ let command = args.next().unwrap();
+ match command.as_str() {
+ "start" => {
+ let mut stream: UnixStream = connect(&name, &prefixed)?;
+ stream.write_all(b"len:13\nop:no\n")?;
+ stream.flush()?;
+ let mut response = String::new();
+ stream.read_to_string(&mut response)?;
+ println!("{}", response);
+ }
+ "noop" | "shutdown" => {
+ // TODO separate shutdown; starting it up just to kill it is daft...
+ let mut stream: UnixStream = connect(&name, &prefixed)?;
+ if command == "shutdown" {
+ stream.write_all(b"len:15\nop:quit\n")?;
+ } else if command == "noop" {
+ stream.write_all(b"len:13\nop:no\n")?;
+ }
+ let mut response = String::new();
+ stream.read_to_string(&mut response)?;
+ println!("{}", response);
+ }
+ "check" => {
+ check(PathBuf::from(args.next().unwrap()), &profile, &prefixed);
+ }
+ _ => todo!("only check, noop, shutdown"),
+ }
+ Ok(())
+}
+
+fn parse_args(args: &mut std::env::Args) -> (nmh::Context, String) {
+ (
+ nmh::init(std::ffi::CString::new(args.next().unwrap()).unwrap()),
+ args.next().unwrap(),
+ )
+}
+
+fn connect(name: &str, prefixed: &nmh::profile::Prefixed) -> std::io::Result<UnixStream> {
+ // 1. Try to connect to running manager.
+ let socket_path = Path::new(prefixed.get_or_err("socket").unwrap());
+ match UnixStream::connect(socket_path) {
+ Err(e) => {
+ match e.kind() {
+ std::io::ErrorKind::NotFound => {}
+ // 2. If not ENOENT, unlink the stale socket name.
+ _ => std::fs::remove_file(&socket_path)?,
+ }
+ // 3. make pipe
+ // 4. fork
+ // TODO if $name-manager is a relative path, look in libexecdir
+ let run_path = Path::new(prefixed.get_or_err("manager").unwrap());
+ // 5.child. folder-manager; prints to stdout if successfully listening
+ let command = std::process::Command::new(run_path)
+ .arg(name)
+ .stdout(std::process::Stdio::piped())
+ .spawn()?;
+ let mut stdout: std::process::ChildStdout = command.stdout.unwrap();
+ // 5.parent read from pipe to child
+ // 6.parent if error, die
+ let mut buf = [0_u8; 1];
+ if stdout.read(&mut buf)? != 1 {
+ todo!("imap-folder-manager didn't report ready")
+ }
+ // 7.parent else, time to connect again! this time, die if error
+ UnixStream::connect(socket_path)
+ }
+ ok => ok,
+ }
+}
+
+fn check(folder_path: PathBuf, profile: &nmh::Profile, prefixed: &nmh::profile::Prefixed) {
+ assert!(folder_path.is_relative());
+
+ let db_path = Path::new(prefixed.get("db").unwrap().trim()).to_path_buf();
+ let fpm = nmhimapstore::FolderPathMapper::new(
+ Path::new(profile.get("Path").unwrap().trim()).to_path_buf(),
+ db_path,
+ );
+ nmhimapstore::check(&nmh::folder::Name::from_utf8(folder_path).unwrap(), fpm).unwrap()
+}
--- /dev/null
+[package]
+name = "nmhimapstore"
+# TODO derive from nmh version
+version = "0.0.0"
+edition = "2018"
+
+[dependencies]
+lmdb-rkv = "0.14"
+lmdb-rkv-sys = "0.11"
+log = "0.4"
+# nmh
+nmh = { path = "../../sbr/rust" }
+nmhimaptypes = { path = "../types" }
--- /dev/null
+use std::io::Write as _;
+
+use nmh::{folder, message};
+use nmhimaptypes::Uid;
+
+use crate::{Error, FolderPathMapper, FolderStore, ReadTransaction, Result, WriteTransaction};
+
+/// For appending messages into a folder.
+/// Start with [FolderStore::append].
+pub struct FolderAppender<'a> {
+ pub highest_uid: Option<Uid>,
+ pub uidnext: Option<Uid>,
+
+ next_msgnum: message::Number,
+ store: FolderStore,
+
+ fpm: &'a FolderPathMapper,
+ folder_name: &'a folder::Name,
+}
+
+impl<'a> FolderAppender<'a> {
+ /// Open a [MessageWriter] to begin writing a a new message in the folder. Until successful
+ /// [MessageWriter::close], the message is not stored. Any message file left behind after
+ /// failure to append a message will be overwritten by the next message append.
+ pub fn append(mut self, uid: Uid) -> Result<MessageWriter<'a>> {
+ let msgnum = self.next_msgnum;
+ self.next_msgnum.increment();
+ let path: std::path::PathBuf = self.fpm.msg_path(self.folder_name, msgnum);
+ MessageWriter {
+ msgnum,
+ uid,
+ appender: self,
+ path,
+ file: None,
+ }
+ .open()
+ }
+
+ pub fn close(self) -> FolderStore {
+ self.store
+ }
+
+ pub(super) fn new(
+ store: FolderStore,
+ fpm: &'a FolderPathMapper,
+ folder_name: &'a folder::Name,
+ ) -> Result<Self> {
+ let highest_uid: Option<Uid>;
+ let mut next_msgnum: message::Number;
+ match store.read_transaction()?.highest()? {
+ Some((uid, msgnum)) => {
+ highest_uid = Some(uid);
+ next_msgnum = msgnum;
+ next_msgnum.increment();
+ }
+ None => {
+ highest_uid = None;
+ next_msgnum = message::Number::one();
+ }
+ }
+ let uidnext: Option<Uid> = crate::read_nonzero_u32(&store.uidnext_path)?.map(Uid::from);
+ Ok(Self {
+ highest_uid,
+ fpm,
+ folder_name,
+ next_msgnum,
+ store,
+ uidnext,
+ })
+ }
+}
+
+/// For writing to new messages in a folder.
+/// Start with [FolderAppender::append].
+pub struct MessageWriter<'a> {
+ appender: FolderAppender<'a>,
+ path: std::path::PathBuf,
+ file: Option<std::fs::File>,
+ msgnum: message::Number,
+ uid: Uid,
+}
+
+impl<'a> MessageWriter<'a> {
+ /// Sink bytes into the message file.
+ pub fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
+ self.file
+ .as_ref()
+ .expect("opened")
+ .write_all(bytes)
+ .map_err(|e| Error::describe(e, format_args!("{:?}", &self.path)))
+ }
+
+ /// See [FolderAppender::append].
+ pub fn close(self) -> Result<(message::Number, FolderAppender<'a>)> {
+ self.file
+ .as_ref()
+ .expect("opened")
+ .sync_all()
+ .map_err(|e| Error::describe(e, format_args!("{:?}", &self.path)))?;
+ let mut txn: WriteTransaction = self.appender.store.write_transaction()?;
+ txn.set_uid_msgnum(self.uid, self.msgnum)?;
+ txn.commit()?;
+ Ok((self.msgnum, self.appender))
+ }
+
+ pub fn abort(self) -> Result<()> {
+ // TODO attempt to unlink file, warn but ignore if fail; we'll clobber it later
+ Ok(())
+ }
+
+ fn open(mut self) -> Result<Self> {
+ self.file = Some(
+ std::fs::File::create(&self.path)
+ .map_err(|e| Error::describe(e, format_args!("{:?}", &self.path)))?,
+ );
+ Ok(self)
+ }
+}
--- /dev/null
+use nmh::{folder, message};
+use nmhimaptypes::Uid;
+
+use crate::{ReadOnlyTransaction, ReadTransaction};
+
+/// Check folder integrity.
+/// Spams stderr with report and panics on error. TODO: stop that
+pub fn check(folder_name: &folder::Name, fpm: crate::FolderPathMapper) -> crate::Result<()> {
+ let mut errors = vec![];
+ let store = crate::FolderStore::do_open(fpm.folder_path(folder_name), fpm.db_path(folder_name))
+ .unwrap();
+ let txn: ReadOnlyTransaction = store.read_transaction()?;
+ for result in txn.message_number_to_uid_cursor()? {
+ let (msgnum, uid): (message::Number, Uid) = result?;
+ // Lookup uid and ensure that results in msgnum.
+ match txn.lookup_message_number(uid)? {
+ None => errors.push(format!("msg {} missing from uid table", msgnum)),
+ Some(msgnum_from_uid) => {
+ if msgnum_from_uid != msgnum {
+ errors.push(format!(
+ "msg table has {}={}, uid has {}={}",
+ msgnum, uid, uid, msgnum_from_uid
+ ))
+ }
+ }
+ }
+ // Ensure msgnum file exists and is not empty.
+ match std::fs::metadata(fpm.msg_path(folder_name, msgnum)) {
+ Err(e) => match e.kind() {
+ std::io::ErrorKind::NotFound => {
+ errors.push(format!("message file {} missing", msgnum))
+ }
+ _ => todo!("stat({:?}): {}", msgnum, e),
+ },
+ Ok(st) => {
+ if st.len() == 0 {
+ errors.push(format!("message file {} empty", msgnum))
+ }
+ }
+ }
+ }
+ for result in txn.uid_to_message_number_cursor()? {
+ let (uid, msgnum): (Uid, message::Number) = result?;
+ // Lookup msgnum and ensure that results in uid.
+ match txn.lookup_uid(msgnum)? {
+ None => errors.push(format!("uid {} missing from msg table", uid)),
+ Some(uid_from_msgnum) => {
+ if uid_from_msgnum != uid {
+ errors.push(format!(
+ "uid table has {}={}, msg has {}={}",
+ uid, msgnum, msgnum, uid_from_msgnum
+ ))
+ }
+ }
+ }
+ }
+ if errors.len() > 0 {
+ Err(crate::Error::new(errors.join("\n")))
+ } else {
+ Ok(())
+ }
+}
--- /dev/null
+//! Storing IMAP mailboxes in nmh folders.
+//!
+//! Start with [MessageNumberView] for a read-only, message-number-only view of an existing folder.
+//! Everything else is in [FolderStore], opening of which requires a server connection in order to
+//! ensure UIDVALIDITY is not violated.
+
+// trait imports
+use std::convert::TryInto as _;
+use std::io::Write as _;
+
+use std::path::{Path, PathBuf};
+
+use nmh::{folder, message, Error};
+use nmhimaptypes::{Uid, UidValidity};
+
+mod append;
+mod check;
+mod mapper;
+mod transaction;
+
+pub use append::{FolderAppender, MessageWriter};
+pub use check::check;
+pub use mapper::FolderPathMapper;
+pub use transaction::{
+ MessageCursor, MessageNumber2UidCursor, MessageNumberCursor, MessageNumberTransaction,
+ ReadOnlyTransaction, ReadTransaction, WriteTransaction,
+};
+
+/// Offers the full capabilities of the folder store, including mapping between local message
+/// numbers and server UIDs. Because that mapping is unstable, UIDVALIDITY is required to open
+/// this. If the server's value differs from the local value, the folder cannot be opened.
+/// In cases where only message numbers are needed and a server connection is undesired,
+/// use [MessageNumberView] instead.
+pub struct FolderStore {
+ lmdb_path: PathBuf,
+ folder_path: PathBuf,
+ environment: lmdb::Environment,
+ msg2uid: lmdb::Database,
+ uid2msg: lmdb::Database,
+ uidnext_path: PathBuf,
+}
+
+impl FolderStore {
+ pub fn create(
+ fpm: &FolderPathMapper,
+ folder_name: &folder::Name,
+ uid_next: Uid,
+ uid_validity: UidValidity,
+ ) -> Result<Self> {
+ let folder_path: PathBuf = fpm.folder_path(folder_name);
+ let db_path: PathBuf = fpm.db_path(folder_name);
+ if db_path.exists() {
+ return Err(Error::new(format!("{:?} exists", db_path)));
+ }
+ Self::do_create(folder_path, db_path, uid_next.into(), uid_validity)
+ }
+
+ pub fn open_or_create(
+ fpm: &FolderPathMapper,
+ folder_name: &folder::Name,
+ uidvalidity: UidValidity,
+ ) -> Result<Self> {
+ let db_path: PathBuf = fpm.db_path(folder_name);
+ if db_path.exists() {
+ Self::open(fpm, folder_name, uidvalidity)
+ } else {
+ let folder_path = fpm.folder_path(folder_name);
+ Self::do_create(folder_path, db_path, 0, uidvalidity)
+ }
+ }
+
+ pub fn open(
+ fpm: &FolderPathMapper,
+ folder_name: &folder::Name,
+ uidvalidity: UidValidity,
+ ) -> Result<Self> {
+ let folder_path: PathBuf = fpm.folder_path(folder_name);
+ let db_path: PathBuf = fpm.db_path(folder_name);
+ if read_uidvalidity(&db_path)? != uidvalidity {
+ todo!("UIDVALIDITY changed");
+ }
+ Self::do_open(folder_path, db_path)
+ }
+
+ /// Returns [nmh::Error] when message number or UID are not in the database.
+ /// Caller is expected to have looked it up first.
+ pub fn delete_message(&self, msgnum: message::Number, uid: Uid) -> Result<()> {
+ let msg_bytes: [u8; 4] = msgnum_to_bytes(msgnum);
+ let uid_bytes: [u8; 4] = uid_to_bytes(uid);
+ let mut txn = self.write_transaction()?;
+ txn.txn
+ .del(self.msg2uid, &msg_bytes, None)
+ .map_err(|e| Error::describe(e, format_args!("Transaction::del")))?;
+ txn.txn
+ .del(self.uid2msg, &uid_bytes, None)
+ .map_err(|e| Error::describe(e, format_args!("Transaction::del")))?;
+ txn.commit()?;
+
+ let path: PathBuf = self.folder_path.join(msgnum.to_string());
+ if let Err(e) = std::fs::remove_file(&path) {
+ match e.kind() {
+ std::io::ErrorKind::NotFound => {}
+ _ => return Err(Error::describe(e, format_args!("unlink({:?})", path))),
+ }
+ }
+ Ok(())
+ }
+
+ /// Open a [FolderAppender] for appending messages to the folder.
+ pub fn append<'a>(
+ self,
+ fpm: &'a FolderPathMapper,
+ folder_name: &'a folder::Name,
+ ) -> Result<FolderAppender<'a>> {
+ FolderAppender::new(self, fpm, folder_name)
+ }
+
+ // TODO move self into ReadOnlyTransaction so opening two transactions fails at compilation rather than runtime
+ pub fn read_transaction(&self) -> Result<ReadOnlyTransaction> {
+ Ok(ReadOnlyTransaction {
+ txn: self.environment.begin_ro_txn().map_err(|e| {
+ Error::describe(
+ e,
+ format_args!("begin read-only transaction in {:?}", self.lmdb_path),
+ )
+ })?,
+ msg2uid: self.msg2uid,
+ uid2msg: self.uid2msg,
+ lmdb_path: &self.lmdb_path,
+ })
+ }
+
+ pub fn write_transaction(&self) -> Result<WriteTransaction> {
+ Ok(WriteTransaction {
+ txn: self
+ .environment
+ .begin_rw_txn()
+ .map_err(|e| Error::describe(e, format_args!("{:?}", self.lmdb_path)))?,
+ msg2uid: self.msg2uid,
+ uid2msg: self.uid2msg,
+ lmdb_path: &self.lmdb_path,
+ })
+ }
+
+ pub fn write_uidnext(&self, uidnext: Uid) -> Result<()> {
+ write_file(&self.uidnext_path, &uidnext.to_string())
+ }
+
+ fn do_open<P: AsRef<Path>>(folder_path: PathBuf, db_path: P) -> Result<Self> {
+ let lmdb_path = db_path.as_ref().join("db");
+ let environment: lmdb::Environment = open_environment(&lmdb_path)?;
+ let msg2uid: lmdb::Database = open_db(&environment, "m2u")?;
+ let uid2msg: lmdb::Database = open_db(&environment, "u2m")?;
+ Ok(Self {
+ lmdb_path,
+ folder_path,
+ environment,
+ msg2uid,
+ uid2msg,
+ uidnext_path: db_path.as_ref().join("uidnext"),
+ })
+ }
+
+ fn do_create(
+ folder_path: PathBuf,
+ db_path: PathBuf,
+ uid_next: u32,
+ uid_validity: UidValidity,
+ ) -> Result<Self> {
+ let lmdb_path = db_path.join("db");
+ std::fs::create_dir_all(&lmdb_path)
+ .map_err(|e| Error::describe(e, format_args!("{:?}", lmdb_path)))?;
+ std::fs::create_dir_all(&folder_path)
+ .map_err(|e| Error::describe(e, format_args!("{:?}", folder_path)))?;
+
+ let uidnext_path = db_path.join("uidnext");
+ write_file(&uidnext_path, &uid_next.to_string())?;
+ write_uidvalidity(&db_path, uid_validity)?;
+
+ let environment: lmdb::Environment = open_environment(&lmdb_path)?;
+ let msg2uid: lmdb::Database = create_db(&environment, "m2u")?;
+ let uid2msg: lmdb::Database = create_db(&environment, "u2m")?;
+
+ Ok(Self {
+ lmdb_path,
+ folder_path,
+ environment,
+ msg2uid,
+ uid2msg,
+ uidnext_path,
+ })
+ }
+}
+
+/// Delete a folder.
+pub fn delete(fpm: &FolderPathMapper, folder_name: &folder::Name) -> Result<()> {
+ let folder_path: PathBuf = fpm.folder_path(folder_name);
+ let db_path: PathBuf = fpm.db_path(folder_name);
+ {
+ let store = FolderStore::do_open(folder_path.clone(), &db_path)?;
+ let txn = store.read_transaction()?;
+ for result in txn.message_number_to_uid_cursor()? {
+ let msgnum: message::Number = result?.0;
+ let m = store.folder_path.join(Path::new(&msgnum.to_string()));
+ if let Err(error) = std::fs::remove_file(&m) {
+ match error.kind() {
+ std::io::ErrorKind::NotFound => {}
+ _ => todo!("{:?}", error),
+ }
+ }
+ }
+ }
+ // TODO ignore ENOENT
+ let seq = fpm.seq_path(folder_name);
+ if let Err(error) = std::fs::remove_file(&seq) {
+ match error.kind() {
+ std::io::ErrorKind::NotFound => {}
+ _ => todo!("{:?}", error),
+ }
+ };
+ // TODO report failure back to rmf which already has messaging for other files in the folder.
+ // and keep going on ENOENT
+ std::fs::remove_dir(folder_path).unwrap();
+ // Nobody better be in the db path so be relentless.
+ std::fs::remove_dir_all(&db_path).unwrap();
+
+ Ok(())
+}
+
+// TODO Use FolderPathMapper to prevent opening FolderStore and then this?
+/// Offers a subset of [FolderStore] operations allowing access only to local
+/// message numbers. Useful when a server connection is undesired.
+pub struct MessageNumberView(FolderStore);
+
+impl MessageNumberView {
+ pub fn open(fpm: &FolderPathMapper, folder_name: &folder::Name) -> Result<Self> {
+ let folder_path: PathBuf = fpm.folder_path(folder_name);
+ let db_path: PathBuf = fpm.db_path(folder_name);
+ Ok(Self(FolderStore::do_open(folder_path, db_path)?))
+ }
+
+ pub fn read_transaction(&self) -> Result<MessageNumberTransaction> {
+ Ok(MessageNumberTransaction {
+ txn: self.0.environment.begin_ro_txn().map_err(|e| {
+ Error::describe(
+ e,
+ format_args!("begin read-only transaction in {:?}", self.0.lmdb_path),
+ )
+ })?,
+ msg2uid: self.0.msg2uid,
+ })
+ }
+}
+
+type Result<T> = std::result::Result<T, Error>;
+
+fn write_uidvalidity(db_path: &Path, uidvalidity: UidValidity) -> Result<()> {
+ write_file(&db_path.join("uidvalidity"), &uidvalidity.to_string())
+}
+
+fn write_file(path: &Path, contents: &str) -> Result<()> {
+ let mut file = std::fs::File::create(&path)
+ .map_err(|e| Error::describe(e, format_args!("{}", path.display())))?;
+ file.write_all(contents.as_bytes())
+ .map_err(|e| Error::describe(e, format_args!("{}", path.display())))?;
+ file.sync_all()
+ .map_err(|e| Error::describe(e, format_args!("{}", path.display())))
+}
+
+fn read_uidvalidity(db_path: &Path) -> Result<UidValidity> {
+ read_nonzero_u32(&db_path.join("uidvalidity"))?
+ .ok_or_else(|| {
+ Error::new(format!(
+ "{} has illegal UIDVALIDITY zero",
+ db_path.display()
+ ))
+ })
+ .map(UidValidity::from)
+}
+
+fn read_nonzero_u32(path: &Path) -> Result<Option<std::num::NonZeroU32>> {
+ Ok(std::num::NonZeroU32::new(
+ std::fs::read_to_string(&path)
+ .map_err(|e| Error::describe(e, format_args!("{}", path.display())))?
+ .parse::<u32>()
+ .map_err(|e| Error::describe(e, format_args!("{}", path.display())))?,
+ ))
+}
+
+fn open_environment(path: &Path) -> Result<lmdb::Environment> {
+ lmdb::Environment::new()
+ .set_max_dbs(2)
+ .set_map_size(1073741824) // TODO
+ .open_with_permissions(path, 0o600)
+ .map_err(|e| Error::describe(e, format_args!("{}", path.display())))
+}
+
+fn open_db(environment: &lmdb::Environment, name: &str) -> Result<lmdb::Database> {
+ environment
+ .open_db(Some(name))
+ .map_err(|e| Error::describe(e, format_args!("{}", name)))
+}
+
+fn create_db(environment: &lmdb::Environment, name: &str) -> Result<lmdb::Database> {
+ environment
+ .create_db(Some(name), lmdb::DatabaseFlags::INTEGER_KEY)
+ .map_err(|e| Error::describe(e, format_args!("{}", name)))
+}
+
+fn msgnum_to_bytes(msgnum: message::Number) -> [u8; 4] {
+ i32::from(msgnum).to_ne_bytes()
+}
+
+fn uid_to_bytes(uid: Uid) -> [u8; 4] {
+ u32::from(uid).to_ne_bytes()
+}
+
+fn bytes_to_uid(uid_bytes: &[u8]) -> Uid {
+ std::num::NonZeroU32::new(u32::from_ne_bytes(uid_bytes.try_into().expect("4 bytes")))
+ .expect("UID > 0")
+ .into()
+}
+
+fn bytes_to_msgnum(msg_bytes: &[u8]) -> message::Number {
+ std::num::NonZeroI32::new(bytes_to_i32(msg_bytes))
+ .expect("message number > 0")
+ .into()
+}
+
+fn bytes_to_i32(msg_bytes: &[u8]) -> i32 {
+ i32::from_ne_bytes(msg_bytes.try_into().expect("4 bytes"))
+}
+
+#[cfg(test)]
+mod tests;
--- /dev/null
+use nmh::{folder, message};
+
+#[derive(Debug)]
+pub struct FolderPathMapper {
+ // E.g. ~/Mail
+ mail_path: std::path::PathBuf,
+ // E.g. ~/Mail/managed/imap
+ //folder_path: std::path::PathBuf,
+ // E.g. ~/.mhimapdata
+ db_path: std::path::PathBuf,
+}
+
+impl FolderPathMapper {
+ pub fn new(mail_path: std::path::PathBuf, db_path: std::path::PathBuf) -> Self {
+ Self { mail_path, db_path }
+ }
+
+ pub fn seq_path(&self, folder: &folder::Name) -> std::path::PathBuf {
+ // TODO .mh_sequences name is configurable
+ self.mail_path.join(folder.as_path()).join(".mh_sequences")
+ }
+
+ pub fn msg_path(&self, folder: &folder::Name, msgnum: message::Number) -> std::path::PathBuf {
+ self.mail_path
+ .join(folder.as_path())
+ .join(msgnum.to_string())
+ }
+
+ pub(super) fn folder_path(&self, folder: &folder::Name) -> std::path::PathBuf {
+ self.mail_path.join(folder.as_path())
+ }
+
+ pub(super) fn db_path(&self, folder: &folder::Name) -> std::path::PathBuf {
+ self.db_path.join(folder.as_path())
+ }
+}
--- /dev/null
+use nmh::message;
+use nmhimaptypes::Uid;
+
+use super::*;
+
+#[test]
+fn msgnum_to_bytes_1() {
+ assert_eq!(ONE_BYTES, msgnum_to_bytes(message::Number::one()));
+}
+
+#[test]
+fn uid_to_bytes_1() {
+ assert_eq!(ONE_BYTES, uid_to_bytes(uid()));
+}
+
+mod bytes_to {
+ use super::*;
+
+ mod uid {
+ use super::*;
+
+ #[test]
+ fn one() {
+ assert_eq!(uid(), bytes_to_uid(&ONE_BYTES));
+ }
+
+ #[test]
+ #[should_panic(expected = "4 bytes: TryFromSliceError(())")]
+ fn zero_bytes() {
+ bytes_to_uid(&[]);
+ }
+
+ #[test]
+ #[should_panic(expected = "4 bytes: TryFromSliceError(())")]
+ fn too_few_bytes() {
+ bytes_to_uid(&[1]);
+ }
+
+ #[test]
+ #[should_panic(expected = "4 bytes: TryFromSliceError(())")]
+ fn too_many_bytes() {
+ bytes_to_uid(&[1, 0, 0, 0, 0]);
+ }
+
+ #[test]
+ #[should_panic(expected = "UID > 0")]
+ fn zero() {
+ bytes_to_uid(&[0, 0, 0, 0]);
+ }
+ }
+
+ mod msgnum {
+ use super::*;
+
+ #[test]
+ fn one() {
+ assert_eq!(message::Number::one(), bytes_to_msgnum(&ONE_BYTES));
+ }
+
+ #[test]
+ #[should_panic(expected = "4 bytes: TryFromSliceError(())")]
+ fn zero_bytes() {
+ bytes_to_msgnum(&[]);
+ }
+
+ #[test]
+ #[should_panic(expected = "4 bytes: TryFromSliceError(())")]
+ fn too_few_bytes() {
+ bytes_to_msgnum(&[1]);
+ }
+
+ #[test]
+ #[should_panic(expected = "4 bytes: TryFromSliceError(())")]
+ fn too_many_bytes() {
+ bytes_to_msgnum(&[1, 0, 0, 0, 0]);
+ }
+
+ #[test]
+ #[should_panic(expected = "message number > 0")]
+ fn zero() {
+ bytes_to_msgnum(&[0, 0, 0, 0]);
+ }
+ }
+}
+
+#[cfg(target_endian = "little")]
+const ONE_BYTES: [u8; 4] = [1, 0, 0, 0];
+// XXX UNTESTED
+#[cfg(target_endian = "big")]
+const ONE_BYTES: [u8; 4] = [0, 0, 0, 1];
+
+fn uid() -> Uid {
+ Uid::from(std::num::NonZeroU32::new(1).unwrap())
+}
--- /dev/null
+use std::num::NonZeroI32;
+
+// trait imports
+use lmdb::Cursor as _;
+use lmdb::Transaction as _;
+
+use nmh::message;
+use nmhimaptypes::Uid;
+
+use crate::{
+ bytes_to_i32, bytes_to_msgnum, bytes_to_uid, msgnum_to_bytes, uid_to_bytes, Error, Result,
+};
+
+mod cursor;
+pub use cursor::{
+ MessageCursor, MessageNumber2UidCursor, MessageNumberCursor, Uid2MessageNumberCursor,
+};
+
+pub struct MessageNumberTransaction<'a> {
+ pub(super) txn: lmdb::RoTransaction<'a>,
+ pub(super) msg2uid: lmdb::Database,
+}
+
+impl<'a> MessageNumberTransaction<'a> {
+ pub fn cursor(&self) -> Result<MessageNumberCursor> {
+ Ok(MessageNumberCursor::new(
+ self.txn
+ .open_ro_cursor(self.msg2uid)
+ .map_err(|e| Error::describe(e, format_args!("open cursor in msg2uid database")))?,
+ ))
+ }
+}
+
+/// Read operations available to perform on read or write transactions.
+pub trait ReadTransaction<'a, T: lmdb::Transaction> {
+ // TODO copy_message only wants highest msgnum; do we need a variant?
+ /// Return the highest [Uid] and [message::Number] in the folder. These probably do not correspond to each other.
+ fn highest(&self) -> Result<Option<(Uid, message::Number)>>;
+
+ fn lookup_message_number(&self, uid: Uid) -> Result<Option<message::Number>>;
+ fn lookup_uid(&self, msgnum: message::Number) -> Result<Option<Uid>>;
+ fn message_number_to_uid_cursor(&self) -> Result<MessageNumber2UidCursor>;
+ fn uid_to_message_number_cursor(&self) -> Result<Uid2MessageNumberCursor>;
+}
+
+pub struct ReadOnlyTransaction<'a> {
+ pub(super) lmdb_path: &'a std::path::Path,
+ pub(super) msg2uid: lmdb::Database,
+ pub(super) uid2msg: lmdb::Database,
+ pub(super) txn: lmdb::RoTransaction<'a>,
+}
+
+impl<'a> ReadTransaction<'a, lmdb::RoTransaction<'a>> for ReadOnlyTransaction<'a> {
+ fn highest(&self) -> Result<Option<(Uid, message::Number)>> {
+ highest(&self.txn, self.msg2uid, self.uid2msg, self.lmdb_path)
+ }
+
+ fn lookup_message_number(&self, uid: Uid) -> Result<Option<message::Number>> {
+ lookup_message_number(&self.txn, self.uid2msg, self.lmdb_path, uid)
+ }
+
+ fn lookup_uid(&self, msgnum: message::Number) -> Result<Option<Uid>> {
+ lookup_uid(&self.txn, self.msg2uid, self.lmdb_path, msgnum)
+ }
+
+ fn message_number_to_uid_cursor(&self) -> Result<MessageNumber2UidCursor> {
+ message_number_to_uid_cursor(&self.txn, self.msg2uid)
+ }
+
+ fn uid_to_message_number_cursor(&self) -> Result<Uid2MessageNumberCursor> {
+ uid_to_message_number_cursor(&self.txn, self.uid2msg)
+ }
+}
+
+pub struct WriteTransaction<'a> {
+ pub(super) lmdb_path: &'a std::path::Path,
+ pub(super) msg2uid: lmdb::Database,
+ pub(super) uid2msg: lmdb::Database,
+ pub(super) txn: lmdb::RwTransaction<'a>,
+}
+
+impl<'a> WriteTransaction<'a> {
+ pub fn set_uid_msgnum(&mut self, uid: Uid, msgnum: message::Number) -> Result<()> {
+ let uid_bytes: [u8; 4] = uid_to_bytes(uid);
+ let msg_bytes: [u8; 4] = msgnum_to_bytes(msgnum);
+ self.txn
+ .put(
+ self.uid2msg,
+ &uid_bytes,
+ &msg_bytes,
+ lmdb::WriteFlags::NO_OVERWRITE,
+ )
+ .map_err(|e| {
+ Error::describe(
+ e,
+ format_args!("set {}={} in uid2msg database", uid, msgnum),
+ )
+ })?;
+ self.txn
+ .put(
+ self.msg2uid,
+ &msg_bytes,
+ &uid_bytes,
+ lmdb::WriteFlags::NO_OVERWRITE,
+ )
+ .map_err(|e| Error::describe(e, format_args!("put in msg2uid database")))
+ }
+
+ pub fn store_message(&mut self, uid: Uid) -> Result<message::Number> {
+ let one_past_highest_msgnum_in_folder = message::Number::from(
+ NonZeroI32::new(1 + highest_msgnum_or_zero(&self.txn, self.msg2uid)?)
+ .expect("1 + anything to be non-zero"),
+ );
+
+ self.set_uid_msgnum(uid, one_past_highest_msgnum_in_folder)?;
+
+ Ok(one_past_highest_msgnum_in_folder)
+ }
+
+ /// Lookup old UID of `msgnum` and delete it, storing a new relation between `new_uid` and `msgnum`.
+ /// Return old UID.
+ pub fn replace_uid_msgnum(&mut self, new_uid: Uid, msgnum: message::Number) -> Result<Uid> {
+ // Lookup old UID.
+ let msg_bytes: [u8; 4] = msgnum_to_bytes(msgnum);
+ let old_uid_bytes: Vec<u8> = self
+ .txn
+ .get(self.msg2uid, &msg_bytes)
+ .map_err(|e| Error::describe(e, format_args!("{:?}", self.lmdb_path)))?
+ .to_owned();
+ let old_uid: Uid = bytes_to_uid(&old_uid_bytes);
+
+ // Delete old UID / msgnum.
+ self.txn
+ .del(self.uid2msg, &old_uid_bytes, None)
+ .map_err(|e| Error::describe(e, format_args!("{:?}", self.lmdb_path)))?;
+ self.txn
+ .del(self.msg2uid, &msg_bytes, None)
+ .map_err(|e| Error::describe(e, format_args!("{:?}", self.lmdb_path)))?;
+
+ // Put new UID / msgnum.
+ self.set_uid_msgnum(new_uid, msgnum)?;
+
+ Ok(old_uid)
+ }
+
+ pub fn commit(self) -> Result<()> {
+ let path = self.lmdb_path;
+ self.txn
+ .commit()
+ .map_err(|e| Error::describe(e, format_args!("commit {:?}", path)))?;
+ Ok(())
+ }
+}
+
+impl<'a> ReadTransaction<'a, lmdb::RwTransaction<'a>> for WriteTransaction<'a> {
+ fn highest(&self) -> Result<Option<(Uid, message::Number)>> {
+ highest(&self.txn, self.msg2uid, self.uid2msg, self.lmdb_path)
+ }
+
+ fn lookup_message_number(&self, uid: Uid) -> Result<Option<message::Number>> {
+ lookup_message_number(&self.txn, self.uid2msg, self.lmdb_path, uid)
+ }
+
+ fn lookup_uid(&self, msgnum: message::Number) -> Result<Option<Uid>> {
+ lookup_uid(&self.txn, self.msg2uid, self.lmdb_path, msgnum)
+ }
+
+ fn message_number_to_uid_cursor(&self) -> Result<MessageNumber2UidCursor> {
+ message_number_to_uid_cursor(&self.txn, self.msg2uid)
+ }
+
+ fn uid_to_message_number_cursor(&self) -> Result<Uid2MessageNumberCursor> {
+ uid_to_message_number_cursor(&self.txn, self.uid2msg)
+ }
+}
+
+fn highest_msgnum_or_zero<T: lmdb::Transaction>(txn: &T, msg2uid: lmdb::Database) -> Result<i32> {
+ let msg2uid_cursor: lmdb::RoCursor = txn
+ .open_ro_cursor(msg2uid)
+ .map_err(|e| Error::describe(e, format_args!("open cursor in msg2uid database")))?;
+ Ok(match msg2uid_cursor.get(None, None, lmdb_sys::MDB_LAST) {
+ Err(error) => {
+ if error != lmdb::Error::NotFound {
+ todo!("cursor.get returned Err {}", error)
+ }
+ 0
+ }
+ Ok((opt_key, _)) => match opt_key {
+ None => todo!("key not returned for MDB_LAST"),
+ Some(key) => bytes_to_i32(key),
+ },
+ })
+}
+
+fn highest<T: lmdb::Transaction>(
+ txn: &T,
+ msg2uid: lmdb::Database,
+ uid2msg: lmdb::Database,
+ db_path: &std::path::Path,
+) -> Result<Option<(Uid, message::Number)>> {
+ let msgnum = match highest_msgnum_or_zero(txn, msg2uid)? {
+ 0 => return Ok(None),
+ n => message::Number::from(
+ NonZeroI32::new(n).ok_or_else(|| Error::new("illegal message number zero"))?,
+ ),
+ };
+ // TODO basically the same as highest_msgnum_or_zero; factor out
+ let uid: Uid = {
+ let uid2msg_cursor: lmdb::RoCursor = txn
+ .open_ro_cursor(uid2msg)
+ .map_err(|e| Error::describe(e, format_args!("{:?}", db_path)))?;
+ match uid2msg_cursor.get(None, None, lmdb_sys::MDB_LAST) {
+ Ok((opt_key, _value)) => match opt_key {
+ Some(key) => bytes_to_uid(key),
+ None => todo!("key not returned for MDB_LAST"),
+ },
+ Err(error) => {
+ if error != lmdb::Error::NotFound {
+ todo!("cursor.get returned Err {}", error)
+ }
+ todo!("db corrupt: found msgnum but not uid")
+ }
+ }
+ };
+ Ok(Some((uid, msgnum)))
+}
+
+fn lookup_message_number<T: lmdb::Transaction>(
+ txn: &T,
+ db: lmdb::Database,
+ db_path: &std::path::Path,
+ uid: Uid,
+) -> Result<Option<message::Number>> {
+ match txn.get(db, &uid_to_bytes(uid)) {
+ Err(e) => {
+ if e != lmdb::Error::NotFound {
+ Err(Error::describe(
+ e,
+ format_args!("{} in {}", uid, db_path.display()),
+ ))
+ } else {
+ Ok(None)
+ }
+ }
+ Ok(value) => Ok(Some(bytes_to_msgnum(value))),
+ }
+}
+
+fn lookup_uid<T: lmdb::Transaction>(
+ txn: &T,
+ db: lmdb::Database,
+ db_path: &std::path::Path,
+ msgnum: message::Number,
+) -> Result<Option<Uid>> {
+ match txn.get(db, &msgnum_to_bytes(msgnum)) {
+ Err(e) => {
+ if e != lmdb::Error::NotFound {
+ Err(Error::describe(
+ e,
+ format_args!("{} in {}", msgnum, db_path.display()),
+ ))
+ } else {
+ Ok(None)
+ }
+ }
+ Ok(value) => Ok(Some(bytes_to_uid(value))),
+ }
+}
+
+fn message_number_to_uid_cursor<T: lmdb::Transaction>(
+ txn: &T,
+ db: lmdb::Database,
+) -> Result<MessageNumber2UidCursor> {
+ // XXX lmdb-rkv bug: The iterator cannot outlive the txn but that is not expressed in the types.
+ Ok(MessageNumber2UidCursor::new(
+ txn.open_ro_cursor(db)
+ .map_err(|e| Error::describe(e, format_args!("Transaction::open_ro_cursor")))?,
+ ))
+}
+
+fn uid_to_message_number_cursor<T: lmdb::Transaction>(
+ txn: &T,
+ db: lmdb::Database,
+) -> Result<Uid2MessageNumberCursor> {
+ // XXX lmdb-rkv bug: The iterator cannot outlive the txn but that is not expressed in the types.
+ Ok(Uid2MessageNumberCursor::new(
+ txn.open_ro_cursor(db)
+ .map_err(|e| Error::describe(e, format_args!("Transaction::open_ro_cursor")))?,
+ ))
+}
--- /dev/null
+use lmdb::Cursor;
+use lmdb_sys::{MDB_NEXT, MDB_SET_RANGE};
+
+use nmh::message;
+use nmhimaptypes::Uid;
+
+use crate::{bytes_to_msgnum, bytes_to_uid, msgnum_to_bytes, Result};
+
+/// Cursor over [message::Number] and something else (unit or [Uid]). Use to write generic
+/// functions which may operate on [crate::FolderStore] or [crate::MessageNumberView].
+pub trait MessageCursor<T> {
+ /// Return the lowest message number in the database greater than or equal to msgnum if any, or
+ /// None if no more remain.
+ /// Set the position for subsequent next calls.
+ fn get(&self, msgnum: message::Number) -> Result<Option<(message::Number, T)>>;
+
+ // TODO Is this really carrying its own weight? Why not just use our Iterator::next?
+ /// Return the next lowest message number greater than that returned by the previous next or get
+ /// call, or None if no more remain.
+ fn next(&self) -> Result<Option<(message::Number, T)>>;
+}
+
+/// This [MessageCursor] provides access to message numbers and their corresponding UIDs. This can
+/// be used as an [Iterator], even after calling [MessageCursor::get].
+pub struct MessageNumber2UidCursor<'a>(lmdb::RoCursor<'a>);
+
+impl<'a> MessageNumber2UidCursor<'a> {
+ pub fn new(cursor: lmdb::RoCursor<'a>) -> Self {
+ Self(cursor)
+ }
+}
+
+type Num2Uid = (message::Number, Uid);
+impl<'a> MessageCursor<Uid> for MessageNumber2UidCursor<'a> {
+ fn get(&self, msgnum: message::Number) -> Result<Option<Num2Uid>> {
+ process_msg2uid_cursor(
+ self.0
+ .get(Some(&msgnum_to_bytes(msgnum)), None, MDB_SET_RANGE),
+ )
+ }
+
+ fn next(&self) -> Result<Option<Num2Uid>> {
+ process_msg2uid_cursor(self.0.get(None, None, MDB_NEXT))
+ }
+}
+
+impl Iterator for MessageNumber2UidCursor<'_> {
+ type Item = Result<(message::Number, Uid)>;
+ fn next(&mut self) -> Option<Self::Item> {
+ to_iter(process_msg2uid_cursor(self.0.get(None, None, MDB_NEXT)))
+ }
+}
+
+/// Provides access to UIDs and their corresponding message numbers. Its purpose is to be an [Iterator].
+pub struct Uid2MessageNumberCursor<'a>(lmdb::RoCursor<'a>);
+
+impl<'a> Uid2MessageNumberCursor<'a> {
+ pub fn new(cursor: lmdb::RoCursor<'a>) -> Self {
+ Self(cursor)
+ }
+}
+
+impl Iterator for Uid2MessageNumberCursor<'_> {
+ type Item = Result<(Uid, message::Number)>;
+ fn next(&mut self) -> Option<Self::Item> {
+ to_iter(process(
+ self.0.get(None, None, MDB_NEXT),
+ bytes_to_uid,
+ bytes_to_msgnum,
+ ))
+ }
+}
+
+/// This [MessageCursor] only provides access to message numbers. This can be used as an
+/// [Iterator], even after calling [MessageCursor::get].
+pub struct MessageNumberCursor<'a>(lmdb::RoCursor<'a>);
+
+impl<'a> MessageNumberCursor<'a> {
+ pub fn new(cursor: lmdb::RoCursor<'a>) -> Self {
+ Self(cursor)
+ }
+}
+
+type OnlyNumber = (message::Number, ());
+impl<'a> MessageCursor<()> for MessageNumberCursor<'a> {
+ fn get(&self, msgnum: message::Number) -> Result<Option<OnlyNumber>> {
+ process_msgnum_cursor_get(
+ self.0
+ .get(Some(&msgnum_to_bytes(msgnum)), None, MDB_SET_RANGE),
+ )
+ }
+
+ fn next(&self) -> Result<Option<OnlyNumber>> {
+ process_msgnum_cursor_get(self.0.get(None, None, MDB_NEXT))
+ }
+}
+
+fn process_msg2uid_cursor(result: lmdb::Result<(Option<&[u8]>, &[u8])>) -> Result<Option<Num2Uid>> {
+ process(result, bytes_to_msgnum, bytes_to_uid)
+}
+
+fn process_msgnum_cursor_get(
+ result: lmdb::Result<(Option<&[u8]>, &[u8])>,
+) -> Result<Option<OnlyNumber>> {
+ process(result, bytes_to_msgnum, |_| ())
+}
+
+fn process<K, V, KF: Fn(&[u8]) -> K, VF: Fn(&[u8]) -> V>(
+ result: lmdb::Result<(Option<&[u8]>, &[u8])>,
+ convert_key: KF,
+ convert_value: VF,
+) -> Result<Option<(K, V)>> {
+ Ok(match result {
+ Err(error) => {
+ if error != lmdb::Error::NotFound {
+ todo!("cursor.get returned Err {}", error)
+ }
+ None
+ }
+ Ok((Some(key), value)) => Some((convert_key(key), convert_value(value))),
+ unexpected => todo!("shouldn't happen {:?}", unexpected),
+ })
+}
+
+fn to_iter<T>(result: Result<Option<T>>) -> Option<Result<T>> {
+ match result {
+ Err(e) => Some(Err(e)),
+ Ok(None) => None,
+ Ok(Some(v)) => Some(Ok(v)),
+ }
+}
--- /dev/null
+[package]
+name = "nmhimaptypes"
+# TODO derive from nmh version
+version = "0.0.0"
+edition = "2018"
+
+[dependencies]
+# nmh
+nmh = { path = "../../sbr/rust" }
--- /dev/null
+pub mod uid;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct Uid(std::num::NonZeroU32);
+
+impl std::fmt::Display for Uid {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl From<std::num::NonZeroU32> for Uid {
+ fn from(n: std::num::NonZeroU32) -> Self {
+ Self(n)
+ }
+}
+
+impl From<Uid> for u32 {
+ fn from(uid: Uid) -> Self {
+ uid.0.get()
+ }
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct UidValidity(std::num::NonZeroU32);
+
+impl std::fmt::Display for UidValidity {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(f)
+ }
+}
+
+impl From<std::num::NonZeroU32> for UidValidity {
+ fn from(n: std::num::NonZeroU32) -> Self {
+ Self(n)
+ }
+}
--- /dev/null
+#[derive(Clone, Debug)]
+pub struct Set(std::collections::BTreeSet<u32>);
+
+// TODO dedup with nmh::message::Set? Better just to wrap sbr structures.
+impl Set {
+ pub fn new() -> Self {
+ Self(std::collections::BTreeSet::new())
+ }
+
+ pub fn append(&mut self, mut other: Self) {
+ self.0.append(&mut other.0)
+ }
+
+ pub fn insert(&mut self, msgnum: u32) -> bool {
+ self.0.insert(msgnum)
+ }
+
+ pub fn contains(&self, msgnum: u32) -> bool {
+ self.0.contains(&msgnum)
+ }
+
+ pub fn iter(&self) -> std::collections::btree_set::Iter<u32> {
+ self.0.iter()
+ }
+}
--- /dev/null
+mod uid {
+ #[test]
+ fn option_no_overhead() {
+ assert_eq!(
+ std::mem::size_of::<u32>(),
+ std::mem::size_of::<Option<nmhimaptypes::Uid>>()
+ );
+ }
+}
+
+mod uid_validity {
+ #[test]
+ fn option_no_overhead() {
+ assert_eq!(
+ std::mem::size_of::<u32>(),
+ std::mem::size_of::<Option<nmhimaptypes::UidValidity>>()
+ );
+ }
+}
--- /dev/null
+[package]
+name = "managed-folders"
+# TODO derive from nmh version
+version = "0.0.0"
+edition = "2018"
+
+[dependencies]
+log = "0.4"
+# nmh
+nmh = { path = "../sbr/rust" }
--- /dev/null
+use nmh::{folder, message};
+
+use crate::{Error, Message};
+
+#[derive(Debug)]
+pub struct UpdateDraft {
+ pub folder: folder::Name,
+ pub msgnum: message::Number,
+}
+
+impl UpdateDraft {
+ pub fn new(message: &Message) -> Result<Self, Error> {
+ Ok(Self {
+ folder: get_as_folder_name(message, "folder")?,
+ msgnum: message
+ .get("msg")
+ .ok_or_else(|| Error::new("msg component missing from request"))?
+ .parse()?,
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct Folder {
+ pub folder: folder::Name,
+}
+
+impl Folder {
+ pub fn new(message: &Message) -> Result<Self, Error> {
+ Ok(Self {
+ folder: get_as_folder_name(message, "folder")?,
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct Pick<'a> {
+ pub folder: folder::Name,
+ pub msgs: Vec<&'a str>,
+ pub need_unseen: bool,
+}
+
+impl<'a> Pick<'a> {
+ pub fn new(message: &'a Message<'a>) -> Result<Self, Error> {
+ Ok(Self {
+ folder: get_as_folder_name(message, "folder")?,
+ msgs: match message.get("msgs") {
+ None => todo!("no msgs component"),
+ Some(value) => non_empty_str_vec(value.trim()),
+ },
+ need_unseen: match message.get("needunseen") {
+ None => todo!("no needunseen component"),
+ Some(value) => match value {
+ "0" => false,
+ "1" => true,
+ _ => todo!("needunseen must be 0 or 1"),
+ },
+ },
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct RefileFile {
+ pub file_path: std::path::PathBuf,
+ pub dst_folder: folder::Name,
+}
+
+impl RefileFile {
+ pub fn new(message: &Message) -> Result<Self, Error> {
+ Ok(Self {
+ file_path: match message.get("file") {
+ None => todo!("no file component"),
+ Some(value) => std::path::Path::new(value).to_path_buf(),
+ },
+ dst_folder: get_as_folder_name(message, "dest")?,
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct Mark<'a> {
+ pub folder: folder::Name,
+ pub msgs: Vec<&'a str>,
+ pub sequences: Vec<&'a str>,
+}
+
+impl<'a> Mark<'a> {
+ pub fn new(message: &'a Message<'a>) -> Result<Self, Error> {
+ Ok(Self {
+ folder: get_as_folder_name(message, "folder")?,
+ msgs: match message.get("msgs") {
+ None => todo!("no msgs component"),
+ Some(value) => non_empty_str_vec(value.trim()),
+ },
+ sequences: match message.get("seqs") {
+ None => todo!("no seqs component"),
+ Some(value) => non_empty_str_vec(value.trim()),
+ },
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct RefileMove<'a> {
+ pub src_folder: folder::Name,
+ pub msgs: Vec<&'a str>,
+ pub dst_folder: folder::Name,
+}
+
+impl<'a> RefileMove<'a> {
+ pub fn new(message: &'a Message<'a>) -> Result<Self, Error> {
+ Ok(Self {
+ src_folder: get_as_folder_name(message, "folder")?,
+ msgs: match message.get("msgs") {
+ None => todo!("no msgs component"),
+ Some(value) => non_empty_str_vec(value.trim()),
+ },
+ dst_folder: get_as_folder_name(message, "dest")?,
+ })
+ }
+}
+
+#[derive(Debug)]
+pub struct Rmm<'a> {
+ pub folder: folder::Name,
+ pub msgs: Vec<&'a str>,
+}
+
+impl<'a> Rmm<'a> {
+ pub fn new(message: &'a Message<'a>) -> Result<Self, Error> {
+ Ok(Self {
+ folder: get_as_folder_name(message, "folder")?,
+ msgs: match message.get("msgs") {
+ None => todo!("no msgs component"),
+ Some(value) => non_empty_str_vec(value.trim()),
+ },
+ })
+ }
+}
+
+fn get_as_folder_name(message: &Message, name: &str) -> Result<folder::Name, Error> {
+ folder::Name::from_utf8(
+ std::path::Path::new(
+ message
+ .get(name)
+ .ok_or_else(|| Error::new(format!("{} component missing from request", name)))?,
+ )
+ .to_path_buf(),
+ )
+}
+
+fn non_empty_str_vec(s: &str) -> Vec<&str> {
+ let result: Vec<&str> = nmh::hacks::split(s).collect();
+ if result.is_empty() {
+ todo!("missing required strs")
+ }
+ result
+}
--- /dev/null
+// TODO this poor man's component parser is used two ways:
+// 1. managed_folders::protocol::read::read_message
+// instead use mh_folder_manager_protocol_read_message
+// 2. imap-folder-manager reading .mh_sequences
+// instead use seq_read (but need to stop it from readdir(3) first)
+// Or just use m_getfld2 directly.
+#[derive(Debug, PartialEq, Eq)]
+pub struct Components<'a> {
+ // TODO this is super lazy because i currently have single-value fields and all multiple-value
+ // fields are ORDERED but this is currently bug-prone.
+ // We don't want BTreeMap; want to preserve insertion order.
+ map: std::collections::HashMap<&'a str, &'a str>,
+ vec: Vec<(&'a str, &'a str)>,
+}
+
+impl<'a> Components<'a> {
+ pub fn new() -> Self {
+ Self {
+ map: std::collections::HashMap::new(),
+ vec: Vec::new(),
+ }
+ }
+
+ pub fn push(&mut self, component: (&'a str, &'a str)) {
+ self.map.insert(component.0, component.1);
+ self.vec.push(component)
+ }
+
+ pub fn get(&self, name: &str) -> Option<&str> {
+ self.map.get(name).copied()
+ }
+
+ pub fn iter(&self) -> std::slice::Iter<(&str, &str)> {
+ self.vec.iter()
+ }
+}
--- /dev/null
+use nmh::Error;
+
+pub mod command;
+
+mod components;
+mod protocol;
+
+pub use components::Components;
+pub use protocol::message::Message;
+pub use protocol::{close_with_error, Command, EmptyResponse, FolderManagerProtocol, IncResponse};
+
+// see TODO on Components
+/// Primitive m_getfld2 replacement. Doesn't even support line folding or missing final newline.
+pub fn parse_components(message: &str) -> Result<Components> {
+ let mut result = Components::new();
+ let mut i = 0;
+ while i < message.len() {
+ let name_end = i + match message[i..].find(':') {
+ None => todo!("error missing colon in component name starting at {}", i),
+ Some(index) => index,
+ };
+ let value_end = name_end
+ + 1
+ + match &message[name_end + 1..].find('\n') {
+ None => todo!("error missing newline in {}", &message[i..]),
+ Some(index) => index,
+ };
+ result.push((&message[i..name_end], &message[name_end + 1..value_end]));
+ i = value_end + 1;
+ }
+ Ok(result)
+}
+
+type Result<T> = std::result::Result<T, Error>;
--- /dev/null
+use std::io::Write;
+use std::os::unix::net::UnixStream;
+
+use nmh::folder;
+
+use crate::{command, Message, Result};
+
+pub mod message;
+mod read;
+mod write;
+
+#[derive(Debug)]
+pub struct FolderManagerProtocol {
+ stream: UnixStream,
+}
+
+impl FolderManagerProtocol {
+ pub fn new(stream: UnixStream) -> Self {
+ FolderManagerProtocol { stream }
+ }
+
+ fn error(mut self, message: &str) -> Result<Self> {
+ self.write_all(&write::write(&Message::from(
+ &[(KEY_STATUS, STATUS_NOTOK), (KEY_ERROR, message)][..],
+ )))?;
+ Ok(self)
+ }
+
+ fn write_all(&mut self, bytes: &[u8]) -> Result<()> {
+ self.stream.write_all(bytes).map_err(nmh::Error::wrap)
+ }
+}
+
+impl FolderManagerProtocol {
+ pub fn read_message(mut self, message_buffer: &mut [u8]) -> Result<(Message, Self)> {
+ match read::read_message(message_buffer, &mut self.stream) {
+ Err(e) => {
+ self.stream
+ .shutdown(std::net::Shutdown::Both)
+ .map_err(crate::Error::wrap)?;
+ Err(e)
+ }
+ Ok(message) => Ok((message, self)),
+ }
+ }
+
+ pub fn read_command<'a>(self, message: &'a Message) -> Result<Command<'a>> {
+ match message.get("op") {
+ None => todo!("no 'op' component"),
+ Some(value) => match value {
+ "no" => Ok(Command::No(EmptyResponse::new(self))),
+ "quit" => {
+ self.close().unwrap();
+ Ok(Command::Quit)
+ }
+ "folder" => Ok(Command::Folder(
+ EmptyResponse::new(self),
+ command::Folder::new(message)?,
+ )),
+ "comp" => Ok(Command::MakeDraft(
+ MessageNumberResponse::new(self),
+ command::Folder::new(message)?,
+ )),
+ "comp-update" => Ok(Command::UpdateDraft(
+ EmptyResponse::new(self),
+ command::UpdateDraft::new(message)?,
+ )),
+ "inc" => Ok(Command::Inc(
+ IncResponse::new(self),
+ command::Folder::new(message)?,
+ )),
+ "make-folder" => Ok(Command::MakeFolder(
+ EmptyResponse::new(self),
+ command::Folder::new(message)?,
+ )),
+ "mark-add" => Ok(Command::MarkAdd(
+ EmptyResponse::new(self),
+ command::Mark::new(message)?,
+ )),
+ "mark-del" => Ok(Command::MarkDel(
+ EmptyResponse::new(self),
+ command::Mark::new(message)?,
+ )),
+ "pick" => Ok(Command::Pick(
+ EmptyResponse::new(self),
+ command::Pick::new(message)?,
+ )),
+ "refile-file" => Ok(Command::RefileFile(
+ EmptyResponse::new(self),
+ command::RefileFile::new(message)?,
+ )),
+ "refile-move" => Ok(Command::RefileMove(
+ EmptyResponse::new(self),
+ command::RefileMove::new(message)?,
+ )),
+ "rmf" => Ok(Command::Rmf(
+ EmptyResponse::new(self),
+ command::Folder::new(message)?,
+ )),
+ "rmm" => Ok(Command::Rmm(
+ EmptyResponse::new(self),
+ command::Rmm::new(message)?,
+ )),
+ _ => todo!("unknown 'op' {}", value),
+ },
+ }
+ }
+
+ pub fn close(self) -> std::io::Result<()> {
+ self.stream.shutdown(std::net::Shutdown::Both)
+ }
+}
+
+#[derive(Debug)]
+pub struct EmptyResponse {
+ protocol: FolderManagerProtocol,
+}
+
+impl EmptyResponse {
+ pub fn ok(mut self) -> Result<FolderManagerProtocol> {
+ self.protocol
+ .stream
+ .write_all(MESSAGE_OK.as_bytes())
+ .unwrap();
+ self.protocol.stream.flush().unwrap();
+ Ok(self.protocol)
+ }
+
+ pub fn error(self, message: &str) -> Result<FolderManagerProtocol> {
+ self.protocol.error(message)
+ }
+
+ fn new(protocol: FolderManagerProtocol) -> Self {
+ Self { protocol }
+ }
+}
+#[derive(Debug)]
+pub struct MessageNumberResponse {
+ protocol: FolderManagerProtocol,
+}
+
+impl MessageNumberResponse {
+ pub fn ok(mut self, msgnum: nmh::message::Number) -> Result<FolderManagerProtocol> {
+ self.protocol
+ .stream
+ .write_all(&write::write(&Message::from(
+ &[(KEY_STATUS, STATUS_OK), (KEY_MSG, &msgnum.to_string())][..],
+ )))
+ .unwrap();
+ self.protocol.stream.flush().unwrap();
+ Ok(self.protocol)
+ }
+
+ pub fn error(self, message: &str) -> Result<FolderManagerProtocol> {
+ self.protocol.error(message)
+ }
+
+ fn new(protocol: FolderManagerProtocol) -> Self {
+ Self { protocol }
+ }
+}
+
+#[derive(Debug)]
+pub struct IncResponse {
+ protocol: FolderManagerProtocol,
+ folder: folder::Name,
+}
+
+impl IncResponse {
+ pub fn message(
+ &mut self,
+ folder: &folder::Name,
+ msgnum: nmh::message::Number,
+ unseen: bool,
+ ) -> Result<()> {
+ let mut m = Message::new();
+ if *folder != self.folder {
+ self.folder = folder.clone();
+ m.push((KEY_STATUS, STATUS_INC_FOLDER));
+ m.push((KEY_FOLDER, folder.to_str()));
+ }
+ let msgnum_s = msgnum.to_string();
+ m.push((KEY_MSG, &msgnum_s));
+ m.push((KEY_UNSEEN, if unseen { "1" } else { "0" }));
+ self.protocol.stream.write_all(&write::write(&m)).unwrap();
+ self.protocol.stream.flush().unwrap();
+ Ok(())
+ }
+
+ pub fn ok(mut self) -> Result<FolderManagerProtocol> {
+ self.protocol
+ .stream
+ .write_all(MESSAGE_OK.as_bytes())
+ .unwrap();
+ Ok(self.protocol)
+ }
+
+ pub fn error(self, message: &str) -> Result<FolderManagerProtocol> {
+ self.protocol.error(message)
+ }
+
+ fn new(protocol: FolderManagerProtocol) -> Self {
+ Self {
+ protocol,
+ folder: folder::Name::from_utf8(std::path::PathBuf::new()).unwrap(),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum Command<'a> {
+ Quit,
+
+ Inc(IncResponse, command::Folder),
+ MakeDraft(MessageNumberResponse, command::Folder),
+
+ // All the rest use EmptyResponse.
+ No(EmptyResponse),
+ Folder(EmptyResponse, command::Folder),
+ MakeFolder(EmptyResponse, command::Folder),
+ MarkAdd(EmptyResponse, command::Mark<'a>),
+ MarkDel(EmptyResponse, command::Mark<'a>),
+ Pick(EmptyResponse, command::Pick<'a>),
+ RefileFile(EmptyResponse, command::RefileFile),
+ RefileMove(EmptyResponse, command::RefileMove<'a>),
+ Rmf(EmptyResponse, command::Folder),
+ Rmm(EmptyResponse, command::Rmm<'a>),
+ UpdateDraft(EmptyResponse, command::UpdateDraft),
+}
+
+pub fn close_with_error(mut stream: UnixStream, message: &str) -> std::io::Result<()> {
+ stream.write_all(&write::write(&Message::from(
+ &[(KEY_STATUS, STATUS_NOTOK), (KEY_ERROR, message)][..],
+ )))?;
+ stream.flush()?;
+ FolderManagerProtocol { stream }.close()
+}
+
+const MESSAGE_OK: &str = "len:17\nstatus:OK\n";
+
+const KEY_ERROR: &str = "error";
+const KEY_FOLDER: &str = "folder";
+const KEY_MSG: &str = "msg";
+const KEY_STATUS: &str = "status";
+const KEY_UNSEEN: &str = "unseen";
+
+const STATUS_NOTOK: &str = "NOTOK";
+const STATUS_OK: &str = "OK";
+const STATUS_INC_FOLDER: &str = "INC_FOLDER";
--- /dev/null
+use crate::Components;
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct Message<'a>(Components<'a>);
+
+impl<'a> Message<'a> {
+ pub fn new() -> Self {
+ Self(Components::new())
+ }
+
+ pub fn push(&mut self, component: (&'a str, &'a str)) {
+ self.0.push(component)
+ }
+
+ pub fn get(&self, name: &str) -> Option<&str> {
+ self.0.get(name)
+ }
+
+ pub fn iter(&self) -> std::slice::Iter<(&str, &str)> {
+ self.0.iter()
+ }
+}
+
+impl Default for Message<'_> {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl<'a> From<&[(&'a str, &'a str)]> for Message<'a> {
+ fn from(components: &[(&'a str, &'a str)]) -> Self {
+ let mut m = Self::new();
+ for c in components {
+ m.push(*c);
+ }
+ m
+ }
+}
+
+impl<'a> From<Components<'a>> for Message<'a> {
+ fn from(components: Components<'a>) -> Self {
+ Self(components)
+ }
+}
--- /dev/null
+use crate::{parse_components, Error, Message, Result};
+
+pub fn read_message<'a, T: std::io::Read>(
+ message_buffer: &'a mut [u8],
+ stream: &mut T,
+) -> Result<Message<'a>> {
+ let (end_of_len, mut bytes_read) = read_line(message_buffer, stream)?;
+ let read_str = std::str::from_utf8(&message_buffer[..bytes_read])?;
+ if bytes_read < 6 {
+ return Err(Error::new(format!(
+ "protocol error: shortest possible message is 6 bytes not {}: {}",
+ bytes_read, read_str
+ )));
+ }
+ if !read_str.starts_with("len:") {
+ return Err(Error::new(format!(
+ "protocol error: must start with len not {}",
+ read_str
+ )));
+ }
+ let len: usize = read_str[4..end_of_len].parse().map_err(|e| {
+ Error::describe(
+ e,
+ format_args!(
+ "protocol error: parse len component of message: {}",
+ read_str
+ ),
+ )
+ })?;
+ while bytes_read < len {
+ bytes_read += read(&mut message_buffer[bytes_read..len], stream)?;
+ }
+ Ok(Message::from(parse_components(std::str::from_utf8(
+ &message_buffer[end_of_len + 1..len],
+ )?)?))
+}
+
+// return (end_of_line, bytes_read)
+fn read_line<T: std::io::Read>(buf: &mut [u8], stream: &mut T) -> Result<(usize, usize)> {
+ let max = std::cmp::min(15, buf.len());
+ let mut bytes_read = 0;
+ // TODO timeout
+ loop {
+ let this_read = read(&mut buf[bytes_read..max], stream)?;
+ if this_read == 0 {
+ return Err(Error::new("protocol error: zero bytes read".to_owned()));
+ }
+ let previous_read = bytes_read;
+ bytes_read += this_read;
+ for i in previous_read..bytes_read {
+ if buf[i] == 10 {
+ return Ok((i, bytes_read));
+ }
+ }
+ if bytes_read == max {
+ return Err(Error::new(format!(
+ "protocol error: can't find newline within {} bytes",
+ max
+ )));
+ }
+ }
+}
+
+fn read<T: std::io::Read>(buf: &mut [u8], stream: &mut T) -> Result<usize> {
+ stream
+ .read(buf)
+ .map_err(|e| Error::describe(e, format_args!("read")))
+}
+
+#[cfg(test)]
+mod tests;
--- /dev/null
+use super::*;
+
+mod read_message {
+ use super::*;
+
+ mod read_buffer {
+ use super::*;
+
+ #[test]
+ fn too_small() {
+ let mut buf = [0 as u8; 1];
+ let s: Vec<u8> = b"len:11\na:b\n".to_vec();
+ assert_eq!(
+ "protocol error: can't find newline within 1 bytes",
+ read_message(&mut buf, &mut std::io::Cursor::new(s))
+ .unwrap_err()
+ .to_string()
+ )
+ }
+
+ #[test]
+ fn zero_sized() {
+ let mut buf = [0 as u8; 0];
+ let s: Vec<u8> = b"len:11\na:b\n".to_vec();
+ assert_eq!(
+ "protocol error: zero bytes read",
+ read_message(&mut buf, &mut std::io::Cursor::new(s))
+ .unwrap_err()
+ .to_string()
+ )
+ }
+ }
+
+ mod message {
+ use super::*;
+
+ #[test]
+ fn empty() {
+ let mut buf = [0 as u8; 64];
+ let s: Vec<u8> = b"".to_vec();
+ assert_eq!(
+ "protocol error: zero bytes read",
+ read_message(&mut buf, &mut std::io::Cursor::new(s))
+ .unwrap_err()
+ .to_string()
+ )
+ }
+
+ #[test]
+ fn too_short() {
+ let mut buf = [0 as u8; 64];
+ let s: Vec<u8> = b"bad\n".to_vec();
+ assert_eq!(
+ "protocol error: shortest possible message is 6 bytes not 4: bad\n",
+ read_message(&mut buf, &mut std::io::Cursor::new(s))
+ .unwrap_err()
+ .to_string()
+ )
+ }
+
+ #[test]
+ fn bad() {
+ let mut buf = [0 as u8; 64];
+ let s: Vec<u8> = b"bad:things\n".to_vec();
+ assert_eq!(
+ "protocol error: must start with len not bad:things\n",
+ read_message(&mut buf, &mut std::io::Cursor::new(s))
+ .unwrap_err()
+ .to_string()
+ )
+ }
+
+ #[test]
+ fn bad_len() {
+ let mut buf = [0 as u8; 64];
+ let s: Vec<u8> = b"len:what\n".to_vec();
+ assert_eq!(
+ "protocol error: parse len component of message: len:what\n: invalid digit found in string",
+ read_message(&mut buf, &mut std::io::Cursor::new(s))
+ .unwrap_err()
+ .to_string()
+ )
+ }
+ }
+}
--- /dev/null
+use std::convert::TryInto;
+
+use crate::Message;
+
+// enough to hold u32::MAX - 4294967295 ought to be enough for anybody
+const LEN_BASE: &str = "len: \n";
+
+pub fn write(message: &Message) -> Vec<u8> {
+ let mut len = LEN_BASE.len();
+ let mut s = LEN_BASE.to_owned();
+ for (name, value) in message.iter() {
+ // + 2 for ':' and '\n'
+ len += name.len() + value.len() + 2;
+ s.push_str(name);
+ s.push_str(":");
+ s.push_str(value);
+ s.push_str("\n");
+ }
+ let len_u32: u32 = len.try_into().expect("4294967295 ought to be enough for anybody");
+ // We already had a beautifully-sized buffer into which to write this but oh well.
+ // If profiling shows a problem, we'll look at it.
+ let len_string = len_u32.to_string();
+ let mut bytes: Vec<u8> = s.into();
+ bytes[4..4 + len_string.len()].clone_from_slice(len_string.as_bytes());
+ bytes
+}
+
+#[cfg(test)]
+mod tests;
--- /dev/null
+use super::*;
+
+#[test]
+fn zero_components() {
+ assert_eq!(
+ "len:15 \n",
+ std::str::from_utf8(&write(&Message::from(&[][..])))
+ .unwrap()
+ .to_owned()
+ );
+}
+
+#[test]
+fn one_component() {
+ assert_eq!(
+ "len:21 \nmsg:1\n",
+ std::str::from_utf8(&write(&Message::from(&[("msg", "1")][..])))
+ .unwrap()
+ .to_owned()
+ );
+}
+
+#[test]
+fn two_components() {
+ assert_eq!(
+ "len:48 \nstatus:NOTOK\nerror:error message\n",
+ std::str::from_utf8(&write(&Message::from(
+ &[("status", "NOTOK"), ("error", "error message")][..]
+ )))
+ .unwrap()
+ .to_owned()
+ );
+}
+
+#[test]
+fn four_components() {
+ assert_eq!(
+ "len:56 \nstatus:INC_FOLDER\nfolder:\nmsg:1\nunseen:1\n",
+ std::str::from_utf8(&write(&Message::from(
+ &[
+ ("status", "INC_FOLDER"),
+ ("folder", ""),
+ ("msg", "1"),
+ ("unseen", "1"),
+ ][..]
+ )))
+ .unwrap()
+ .to_owned()
+ );
+}
--- /dev/null
+use managed_folders::{parse_components, Message};
+
+#[test]
+fn simple_error() {
+ let mut m = Message::new();
+ m.push(("status", "NOTOK"));
+ m.push(("error", "error message"));
+ assert_eq!(
+ &m,
+ &Message::from(parse_components("status:NOTOK\nerror:error message\n").unwrap())
+ );
+}
--- /dev/null
+#!/bin/sh
+CARGO_TARGET_DIR='@CARGO_TARGET_DIR@' exec '@RLS@' "$@"
*/
if (space && !metachar) {
- char **split;
p = mh_xstrdup(command);
- split = brkstring(p, " \t", NULL);
+ char *const *const split = brkstring(p, " \t", NULL);
if (split[0] == NULL) {
die("Invalid blank command found");
}
/*
* static prototypes
*/
-static int brkany (char, char *);
+static int brkany (char, char const *);
-char **
-brkstring (char *str, char *brksep, char *brkterm)
+char *const *
+brkstring (char *const str, char const *const brksep, char const *const brkterm)
{
int i;
char c, *s;
*/
static int
-brkany (char c, char *str)
+brkany (char const c, char const *const str)
{
return str && c && strchr(str, c);
}
* COPYRIGHT file in the root directory of the nmh distribution for
* complete copyright information. */
-char **brkstring(char *, char *, char *) NONNULL(1);
+/* Break up str on characters in brksep, terminated by brkterm.
+ * Each character in str that exists in brksep is replace with a null byte.
+ * Processing stops at the first character in str that exists in brkterm.
+ * Directly mutates str.
+ * Returns pointer to private, static array of pointers into str. */
+char *const *brkstring(char *str, char const *brksep, char const *brkterm) NONNULL(1);
*/
#include "h/mh.h"
-#include "client.h"
-#include "h/mts.h"
#include "h/utils.h"
#include <fcntl.h>
#include <sys/socket.h>
int
-client (char *server, char *service, char *response, int len_response,
- int debug)
+client (char const *const server, char const *const service, char *const response, int const len_response,
+ int const debug)
{
int sd, rc;
struct addrinfo hints, *res, *ai;
* COPYRIGHT file in the root directory of the nmh distribution for
* complete copyright information. */
-int client(char *, char *, char *, int, int);
+int client(char const *, char const *, char *, int, int);
--- /dev/null
+/* folder_manager_client.c
+ *
+ * This code is Copyright (c) 2020, by the authors of nmh. See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+#include "h/mh.h"
+
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <sys/un.h>
+
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "h/folder_manager_protocol.h"
+#include "h/managed_folders.h"
+#include "h/utils.h"
+#include "sbr/error.h"
+#include "sbr/getcpy.h"
+#include "sbr/m_getfld.h"
+#include "sbr/trimcpy.h"
+
+struct mh_folder_manager_client {
+ int skt;
+ FILE *readf, *writef;
+ svector_t response;
+ char *message_buf;
+ size_t message_buf_len;
+ mh_managed_folder *mmf;
+};
+
+struct mh_folder_manager_inc_context {
+ mh_managed_folder_inc_page page;
+ mh_folder_manager_client *mfmc;
+ mh_folder_manager_protocol_read_message_t rmt;
+};
+
+static char const OP_MAKE_DRAFT[] = "op:comp\n";
+static char const OP_FOLDER[] = "op:folder\n";
+static char const OP_MAKE_FOLDER[] = "op:make-folder\n";
+static char const OP_MARK_ADD[] = "op:mark-add\n";
+static char const OP_MARK_DEL[] = "op:mark-del\n";
+static char const OP_RMF[] = "op:rmf\n";
+static char const OP_RMM[] = "op:rmm\n";
+static char const OP_UPDATE_DRAFT[] = "op:comp-update\n";
+static char const OP_INC[] = "op:inc\n";
+static char const OP_PICK[] = "op:pick\n";
+static char const OP_REFILE_FILE[] = "op:refile-file\n";
+static char const OP_REFILE_MOVE[] = "op:refile-move\n";
+static char const KEY_FILE[] = "file:";
+static char const KEY_FOLDER[] = "folder:";
+static char const KEY_DEST[] = "dest:";
+static char const KEY_MSG[] = "msg:";
+static char const KEY_MSGS[] = "msgs:";
+static char const KEY_NEED_UNSEEN[] = "needunseen:";
+static char const KEY_SEQS[] = "seqs:";
+
+static bool
+read_message(mh_folder_manager_client *const mfmc);
+
+static bool
+write_message(mh_folder_manager_client *const mfmc, char const *const name1, ...) ENDNULL;
+
+static char *
+msgs_str(struct msgs_array const msgs);
+
+static char *
+seqs_str(struct svector const *const sequences);
+
+bool
+mh_folder_manager_connect(
+ mh_folder_manager_client **const result,
+ mh_managed_folder *const mmf)
+{
+ struct sockaddr_un sun = { 0 };
+ mh_folder_manager_client* mfmc;
+ // TODO hang this off the mmfs so we can free everything in one bang?
+ NEW(mfmc);
+ mfmc->skt = -1;
+ mfmc->readf = mfmc->writef = NULL;
+ mfmc->response = NULL;
+ mfmc->message_buf = NULL;
+ mfmc->message_buf_len = 0;
+ mfmc->mmf = mmf;
+ *result = mfmc;
+
+ char const *socket_path = mh_managed_folder_socket_path(mmf);
+ sun.sun_family = AF_UNIX;
+ // TODO ensure strlen(socket_path) < sizeof sun.sun_path at configuration load time
+ strncpy(sun.sun_path, socket_path, strlen(socket_path));
+ mfmc->skt = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (mfmc->skt == -1) {
+ // TODO return errors rather than abort
+ adios(socket_path, "socket");
+ }
+ // 1. Try to connect to running manager.
+ if (connect(mfmc->skt, (struct sockaddr*)&sun, sizeof sun) == -1) {
+ if (errno != ENOENT) {
+ // 2. If not ENOENT, unlink the stale socket name.
+ if (unlink(socket_path) < 0)
+ adios(socket_path, "unable to unlink");
+ }
+ // 3. make pipe
+ int fds[2];
+ if (pipe(fds) < 0)
+ adios ("pipe", "unable to");
+ // 4. fork
+ char const *path = mh_managed_folder_manager_path(mmf);
+ switch (fork()) {
+ case -1:
+ adios("fork", "unable to");
+ case 0:
+ // 5.child. folder-manager; prints to stdout if successfully listening
+ if (dup2(fds[1], 1) < 0)
+ adios("pipe to child stdout", "dup2");
+ if (execlp(path, "imap-folder-manager", mh_managed_folder_name(mmf), NULL) < 0)
+ adios(path, "unable to exec");
+ break;
+ default: {
+ if (close(fds[1]) < 0)
+ adios("pipe", "close");
+ // 5. parent read from pipe to child
+ char buf[1];
+ int status = read(fds[0], buf, 1);
+ // 6.parent if error, die
+ if (status < 0)
+ adios("folder manager", "unable to read from");
+ if (status < 1)
+ adios("folder manager", "read too small from");
+ // 7.parent else, time to connect again! this time, die if error
+ if (connect(mfmc->skt, (struct sockaddr*)&sun, sizeof sun) == -1)
+ adios(socket_path, "unable to connect");
+ break; }
+ }
+ }
+
+ mfmc->readf = fdopen(mfmc->skt, "r");
+ if (mfmc->readf == NULL) {
+ adios(socket_path, "fdopen for reading");
+ }
+
+ mfmc->writef = fdopen(mfmc->skt, "w");
+ if (mfmc->writef == NULL) {
+ adios(socket_path, "fdopen for writing");
+ }
+
+ return true;
+}
+
+bool
+mh_folder_manager_close(mh_folder_manager_client *const mfmc)
+{
+ bool success = true;
+ free(mfmc->message_buf);
+ if (fclose(mfmc->readf) < 0) {
+ perror("TODO fclose");
+ //set_err(mfmc->mmfs, TODO);
+ success = false;
+ }
+ free(mfmc);
+ return success;
+}
+
+int
+mh_folder_manager_create_draft(
+ mh_folder_manager_client *const mfmc,
+ char const *const draft_folder)
+{
+ if (!write_message(
+ mfmc,
+ OP_MAKE_DRAFT,
+ KEY_FOLDER, draft_folder, "\n",
+ NULL)) {
+ return 0;
+ }
+ if (!read_message(mfmc)) {
+ return 0;
+ }
+ int msgnum = 0;
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ char const *const name = svector_read_at(mfmc->response, i++);
+ char *const value = svector_read_at(mfmc->response, i);
+ if (strcmp(name, "msg") == 0) {
+ errno = 0;
+ msgnum = strtol(value, NULL, 10);
+ if (errno != 0) {
+ fprintf(stderr, "TODO failed to convert msgnum from %s\n", value);
+ return 0;
+ }
+ if (msgnum == 0) {
+ fputs("TODO msgnum mustn't be 0\n", stderr);
+ return 0;
+ }
+ break;
+ }
+ }
+ if (msgnum == 0) {
+ // error - manager didn't return "msg" component
+ return 0;
+ }
+ return msgnum;
+}
+
+bool
+mh_folder_manager_update_draft(
+ mh_folder_manager_client *const mfmc,
+ char const *const draft_folder,
+ int const msgnum)
+{
+ char msgnum_buf[32];
+ sprintf(msgnum_buf, "%d\n", msgnum);
+ if (!write_message(
+ mfmc,
+ OP_UPDATE_DRAFT,
+ KEY_FOLDER, draft_folder, "\n",
+ KEY_MSG, msgnum_buf, /* msgnum_buf has final newline */
+ NULL)) {
+ return false;
+ }
+
+ free_svector_and_elements(&mfmc->response);
+ mfmc->response = svector_create(16);
+
+ mh_folder_manager_protocol_read_message_t rmt;
+ rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ if (!mh_folder_manager_protocol_read_message(mfmc->response, &rmt)) {
+ /* Not adding details for LENERR/FMTERR because m_getfld2 already wrote
+ * advise message to stderr. */
+ mh_managed_folder_set_err(mfmc->mmf, "see above");
+ return false;
+ }
+
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ if (strcmp(svector_read_at(mfmc->response, i++), "status") == 0) {
+ if (strcmp(svector_read_at(mfmc->response, i), "OK") != 0) {
+ fprintf(stderr, "TODO handle error message\n");
+ return false;
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+bool
+mh_folder_manager_make_folder(
+ mh_folder_manager_client *const mfmc,
+ char const *const folder)
+{
+ if (!write_message(
+ mfmc,
+ OP_MAKE_FOLDER,
+ KEY_FOLDER, folder, "\n",
+ NULL)) {
+ return false;
+ }
+ if (!read_message(mfmc)) {
+ return false;
+ }
+ int status = NOTOK;
+ char const *error = NULL;
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ char const *key = svector_read_at(mfmc->response, i++);
+ char const *val = svector_read_at(mfmc->response, i);
+ if (strcmp(key, "status") == 0) {
+ if (strcmp(val, "OK") == 0) {
+ status = OK;
+ }
+ } else if (strcmp(key, "error") == 0) {
+ error = val;
+ }
+ }
+ if (status == OK) {
+ if (error != NULL) {
+ // TODO don't spam stderr
+ fprintf(stderr, "folder manager said OK but also gave error message: %s\n", error);
+ mh_managed_folder_set_err(mfmc->mmf, "folder manager said OK but also gave error message");
+ return false;
+ }
+ return true;
+ }
+ if (error == NULL) {
+ mh_managed_folder_set_err(mfmc->mmf, "unspecified error from folder manager");
+ } else {
+ mh_managed_folder_set_err(mfmc->mmf, error);
+ }
+ return false;
+}
+
+static mh_folder_manager_inc_context *
+new_inc_page(mh_folder_manager_client *const mfmc)
+{
+ mh_folder_manager_inc_context *result;
+ NEW(result);
+ result->page.done = false;
+ result->page.folder = NULL;
+ result->mfmc = mfmc;
+ result->rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ return result;
+}
+
+mh_folder_manager_inc_context *
+mh_folder_manager_inc(mh_folder_manager_client *const mfmc, char const *const folder)
+{
+ if (!write_message(
+ mfmc,
+ OP_INC,
+ KEY_FOLDER, folder, "\n",
+ NULL)) {
+ return NULL;
+ }
+ return new_inc_page(mfmc);
+}
+
+mh_managed_folder_inc_page *
+mh_folder_manager_inc_next(mh_folder_manager_inc_context *const mfinc)
+{
+ if (mfinc->page.done) {
+ fprintf(stderr, "TODO can't call after done!\n");
+ return NULL;
+ }
+
+ free_svector_and_elements(&mfinc->mfmc->response);
+ mfinc->mfmc->response = svector_create(16);
+
+ if (!mh_folder_manager_protocol_read_message(mfinc->mfmc->response, &mfinc->rmt)) {
+ /* Not adding details for LENERR/FMTERR because m_getfld2 already wrote
+ * advise message to stderr. */
+ mh_managed_folder_set_err(mfinc->mfmc->mmf, "see above");
+ return NULL;
+ }
+
+ int status = OK;
+ char const *error = NULL;
+ for (size_t i = 0; i < svector_size(mfinc->mfmc->response); i++) {
+ char const *const name = svector_read_at(mfinc->mfmc->response, i++);
+ char *const value = svector_read_at(mfinc->mfmc->response, i);
+ if (strcmp(name, "status") == 0) {
+ if (strcmp(value, "OK") == 0) {
+ mfinc->page.done = true;
+ m_getfld_state_destroy(&mfinc->rmt.getfld_ctx);
+ } else if (strcmp(value, "NOTOK") == 0) {
+ status = NOTOK;
+ } else if (strcmp(value, "INC_FOLDER") != 0) {
+ inform("folder manager status=%s", value);
+ mh_managed_folder_set_err(mfinc->mfmc->mmf, "illegal folder manager status");
+ return NULL;
+ }
+ } else if (strcmp(name, "folder") == 0) {
+ free(mfinc->page.folder);
+ mfinc->page.folder = mh_xstrdup(value);
+ } else if (strcmp(name, "msg") == 0) {
+ errno = 0;
+ mfinc->page.msg = strtol(value, NULL, 10);
+ if (errno != 0) {
+ fprintf(stderr, "TODO failed to convert msgnum from %s\n", value);
+ return NULL;
+ }
+ } else if (strcmp(name, "unseen") == 0) {
+ mfinc->page.unseen = *value == '1';
+ } else if (strcmp(name, "error") == 0) {
+ error = value;
+ } else if (strcmp(name, "len") != 0) {
+ fprintf(stderr, "TODO unknown response component %s\n", name);
+ return NULL;
+ }
+ }
+
+ if (status == NOTOK) {
+ mh_managed_folder_set_err(mfinc->mfmc->mmf,
+ error == NULL ? "NOTOK status without error message" : error);
+ return NULL;
+ } else if (error != NULL) {
+ // TODO complain about error status:OK
+ }
+
+ return &mfinc->page;
+}
+
+bool
+mh_folder_manager_folder_read(mh_folder_manager_client *const mfmc, char const *const folder)
+{
+ if (!write_message(
+ mfmc,
+ OP_FOLDER,
+ KEY_FOLDER, folder, "\n",
+ NULL)) {
+ return false;
+ }
+ free_svector_and_elements(&mfmc->response);
+ mfmc->response = svector_create(16);
+ mh_folder_manager_protocol_read_message_t rmt;
+ rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ if (!mh_folder_manager_protocol_read_message(mfmc->response, &rmt)) {
+ /* Not adding details for LENERR/FMTERR because m_getfld2 already wrote
+ * advise message to stderr. */
+ mh_managed_folder_set_err(mfmc->mmf, "see above");
+ return false;
+ }
+
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ if (strcmp(svector_read_at(mfmc->response, i++), "status") == 0) {
+ if (strcmp(svector_read_at(mfmc->response, i), "OK") != 0) {
+ fprintf(stderr, "TODO handle error message\n");
+ return false;
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+bool
+mh_folder_manager_refile_move(
+ mh_folder_manager_client *const mfmc,
+ char const *const src_folder,
+ struct msgs_array const msgs,
+ char const *const dst_folder)
+{
+ char *msgs_buf = msgs_str(msgs);
+ if (!write_message(
+ mfmc,
+ OP_REFILE_MOVE,
+ KEY_FOLDER, src_folder, "\n",
+ KEY_MSGS, msgs_buf, /* msgs_buf has final newline */
+ KEY_DEST, dst_folder, "\n",
+ NULL)) {
+ free(msgs_buf);
+ return false;
+ }
+ free(msgs_buf);
+
+ free_svector_and_elements(&mfmc->response);
+ mfmc->response = svector_create(16);
+ mh_folder_manager_protocol_read_message_t rmt;
+ rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ if (!mh_folder_manager_protocol_read_message(mfmc->response, &rmt)) {
+ /* Not adding details for LENERR/FMTERR because m_getfld2 already wrote
+ * advise message to stderr. */
+ mh_managed_folder_set_err(mfmc->mmf, "see above");
+ return false;
+ }
+
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ if (strcmp(svector_read_at(mfmc->response, i++), "status") == 0) {
+ if (strcmp(svector_read_at(mfmc->response, i), "OK") != 0) {
+ fprintf(stderr, "TODO handle error message\n");
+ return false;
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+bool
+mh_folder_manager_refile_file(
+ mh_folder_manager_client *const mfmc,
+ char const *const src_file,
+ char const *const dst_folder)
+{
+ if (!write_message(
+ mfmc,
+ OP_REFILE_FILE,
+ KEY_FILE, src_file, "\n",
+ KEY_DEST, dst_folder, "\n",
+ NULL)) {
+ return false;
+ }
+
+ free_svector_and_elements(&mfmc->response);
+ mfmc->response = svector_create(16);
+ mh_folder_manager_protocol_read_message_t rmt;
+ rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ if (!mh_folder_manager_protocol_read_message(mfmc->response, &rmt)) {
+ /* Not adding details for LENERR/FMTERR because m_getfld2 already wrote
+ * advise message to stderr. */
+ mh_managed_folder_set_err(mfmc->mmf, "see above");
+ return false;
+ }
+
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ if (strcmp(svector_read_at(mfmc->response, i++), "status") == 0) {
+ if (strcmp(svector_read_at(mfmc->response, i), "OK") != 0) {
+ fprintf(stderr, "TODO handle error message\n");
+ return false;
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+bool
+mh_folder_manager_pick(
+ mh_folder_manager_client *const mfmc,
+ char const *const folder,
+ struct msgs_array const msgs,
+ int const need_unseen)
+{
+ if (need_unseen < 0 || need_unseen > 1) {
+ die("mh_folder_manager_pick need_unseen must be 0 or 1");
+ }
+ char *msgs_buf = msgs_str(msgs);
+ if (!write_message(
+ mfmc,
+ OP_PICK,
+ KEY_FOLDER, folder, "\n",
+ KEY_MSGS, msgs_buf, /* msgs_buf has final newline */
+ KEY_NEED_UNSEEN, (need_unseen ? "1\n" : "0\n"),
+ NULL)) {
+ free(msgs_buf);
+ return false;
+ }
+ free(msgs_buf);
+
+ free_svector_and_elements(&mfmc->response);
+ mfmc->response = svector_create(16);
+ mh_folder_manager_protocol_read_message_t rmt;
+ rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ if (!mh_folder_manager_protocol_read_message(mfmc->response, &rmt)) {
+ /* Not adding details for LENERR/FMTERR because m_getfld2 already wrote
+ * advise message to stderr. */
+ mh_managed_folder_set_err(mfmc->mmf, "see above");
+ return false;
+ }
+
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ if (strcmp(svector_read_at(mfmc->response, i++), "status") == 0) {
+ if (strcmp(svector_read_at(mfmc->response, i), "OK") != 0) {
+ fprintf(stderr, "TODO handle error message\n");
+ return false;
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+bool
+mh_folder_manager_rmf(mh_folder_manager_client *const mfmc, char const *const folder)
+{
+ if (!write_message(
+ mfmc,
+ OP_RMF,
+ KEY_FOLDER, folder, "\n",
+ NULL)) {
+ return false;
+ }
+
+ free_svector_and_elements(&mfmc->response);
+ mfmc->response = svector_create(16);
+ mh_folder_manager_protocol_read_message_t rmt;
+ rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ if (!mh_folder_manager_protocol_read_message(mfmc->response, &rmt)) {
+ /* Not adding details for LENERR/FMTERR because m_getfld2 already wrote
+ * advise message to stderr. */
+ mh_managed_folder_set_err(mfmc->mmf, "see above");
+ return false;
+ }
+
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ if (strcmp(svector_read_at(mfmc->response, i++), "status") == 0) {
+ if (strcmp(svector_read_at(mfmc->response, i), "OK") != 0) {
+ fprintf(stderr, "TODO handle error message\n");
+ return false;
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+bool
+mh_folder_manager_rmm(mh_folder_manager_client *const mfmc, char const *const folder, struct msgs_array const msgs)
+{
+ char *msgs_buf = msgs_str(msgs);
+ if (!write_message(
+ mfmc,
+ OP_RMM,
+ KEY_FOLDER, folder, "\n",
+ KEY_MSGS, msgs_buf, /* msgs_buf has final newline */
+ NULL)) {
+ free(msgs_buf);
+ return false;
+ }
+ free(msgs_buf);
+
+ free_svector_and_elements(&mfmc->response);
+ mfmc->response = svector_create(16);
+ mh_folder_manager_protocol_read_message_t rmt;
+ rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ if (!mh_folder_manager_protocol_read_message(mfmc->response, &rmt)) {
+ /* Not adding details for LENERR/FMTERR because m_getfld2 already wrote
+ * advise message to stderr. */
+ mh_managed_folder_set_err(mfmc->mmf, "see above");
+ return false;
+ }
+
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ if (strcmp(svector_read_at(mfmc->response, i++), "status") == 0) {
+ if (strcmp(svector_read_at(mfmc->response, i), "OK") != 0) {
+ fprintf(stderr, "TODO handle error message\n");
+ return false;
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+bool
+mh_folder_manager_mark_add(
+ mh_folder_manager_client *const mfmc,
+ char const *const folder,
+ struct msgs_array const msgs,
+ struct svector const *const sequences)
+{
+ char *msgs_buf = msgs_str(msgs);
+ char *seqs_buf = seqs_str(sequences);
+ if (!write_message(
+ mfmc,
+ OP_MARK_ADD,
+ KEY_FOLDER, folder, "\n",
+ KEY_MSGS, msgs_buf, /* msgs_buf has final newline */
+ KEY_SEQS, seqs_buf, /* seqs_buf has final newline */
+ NULL)) {
+ free(msgs_buf);
+ free(seqs_buf);
+ return false;
+ }
+ free(msgs_buf);
+ free(seqs_buf);
+
+ free_svector_and_elements(&mfmc->response);
+ mfmc->response = svector_create(16);
+ mh_folder_manager_protocol_read_message_t rmt;
+ rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ if (!mh_folder_manager_protocol_read_message(mfmc->response, &rmt)) {
+ /* Not adding details for LENERR/FMTERR because m_getfld2 already wrote
+ * advise message to stderr. */
+ mh_managed_folder_set_err(mfmc->mmf, "see above");
+ return false;
+ }
+
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ if (strcmp(svector_read_at(mfmc->response, i++), "status") == 0) {
+ if (strcmp(svector_read_at(mfmc->response, i), "OK") != 0) {
+ fprintf(stderr, "TODO handle error message\n");
+ return false;
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+bool
+mh_folder_manager_mark_del(
+ mh_folder_manager_client *const mfmc,
+ char const *const folder,
+ struct msgs_array const msgs,
+ struct svector const *const sequences)
+{
+ char *msgs_buf = msgs_str(msgs);
+ char *seqs_buf = seqs_str(sequences);
+ if (!write_message(
+ mfmc,
+ OP_MARK_DEL,
+ KEY_FOLDER, folder, "\n",
+ KEY_MSGS, msgs_buf, /* msgs_buf has final newline */
+ KEY_SEQS, seqs_buf, /* seqs_buf has final newline */
+ NULL)) {
+ free(msgs_buf);
+ free(seqs_buf);
+ return false;
+ }
+ free(msgs_buf);
+ free(seqs_buf);
+
+ free_svector_and_elements(&mfmc->response);
+ mfmc->response = svector_create(16);
+ mh_folder_manager_protocol_read_message_t rmt;
+ rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ if (!mh_folder_manager_protocol_read_message(mfmc->response, &rmt)) {
+ /* Not adding details for LENERR/FMTERR because m_getfld2 already wrote
+ * advise message to stderr. */
+ mh_managed_folder_set_err(mfmc->mmf, "see above");
+ return false;
+ }
+
+ for (size_t i = 0; i < svector_size(mfmc->response); i++) {
+ if (strcmp(svector_read_at(mfmc->response, i++), "status") == 0) {
+ if (strcmp(svector_read_at(mfmc->response, i), "OK") != 0) {
+ fprintf(stderr, "TODO handle error message\n");
+ return false;
+ }
+ break;
+ }
+ }
+ return true;
+}
+
+// TODO why is this not all inside mh_folder_manager_protocol_read_message
+static bool
+read_message(mh_folder_manager_client *const mfmc)
+{
+ free_svector_and_elements(&mfmc->response);
+ mfmc->response = svector_create(16);
+
+ mh_folder_manager_protocol_read_message_t rmt;
+ rmt.getfld_ctx = m_getfld_state_init(mfmc->readf);
+ const bool success = mh_folder_manager_protocol_read_message(mfmc->response, &rmt);
+ m_getfld_state_destroy(&rmt.getfld_ctx);
+ return success;
+}
+
+static void
+allocate_buffer(mh_folder_manager_client *const mfmc, size_t const len)
+{
+ if (mfmc->message_buf_len < len) {
+ if (mfmc->message_buf_len == 0)
+ mfmc->message_buf_len = 1024;
+ for (; mfmc->message_buf_len < len; mfmc->message_buf_len *= 2);
+ free(mfmc->message_buf);
+ mfmc->message_buf = mh_xmalloc(mfmc->message_buf_len);
+ }
+}
+
+static bool
+write_message(mh_folder_manager_client *const mfmc, char const *const name1, ...)
+{
+ // Iterate over parameters the first time adding up the buffer size.
+ va_list list;
+ size_t len = 0;
+ {
+ va_start(list, name1);
+ char const *cp = name1;
+ while (cp != NULL) {
+ // Recomputing lengths of all the names which we already knew.
+ // Not hard to fix that, but we're just going to replace this
+ // with calls into the Rust implementation anyway.
+ len += strlen(cp);
+ cp = va_arg(list, char const *);
+ }
+ va_end(list);
+ }
+
+ allocate_buffer(mfmc, len);
+
+ // Copy. (TODO good fit for writev(2) instead)
+ {
+ char *mp = mfmc->message_buf;
+ va_start(list, name1);
+ char const *cp = name1;
+ while (cp != NULL) {
+ mp = stpcpy(mp, cp);
+ cp = va_arg(list, char const *);
+ }
+ va_end(list);
+ }
+ return mh_folder_manager_protocol_write_message(mfmc->writef, mfmc->message_buf, len);
+}
+
+static char *
+msgs_str(struct msgs_array const msgs)
+{
+ // Start with the number of bytes we add: one space after each msg but the last, which gets LF.
+ size_t msgs_len = msgs.size;
+ for (int i = 0; i < msgs.size; i++) {
+ msgs_len += strlen(msgs.msgs[i]);
+ }
+ char *const result = mh_xmalloc(msgs_len);
+ char *mp = result;
+ for (int i = 0; i < msgs.size; i++) {
+ mp = stpcpy(mp, msgs.msgs[i]);
+ mp = stpcpy(mp, " ");
+ }
+ if (mp == result) {
+ stpcpy(result, "\n");
+ } else {
+ *(mp - 1) = '\n';
+ }
+ return result;
+}
+
+static char *
+seqs_str(struct svector const *const sequences)
+{
+ // Start with the number of bytes we add: one space after each seq but the last, which gets LF.
+ size_t seqs_len = svector_size(sequences);
+ for (size_t i = 0; i < svector_size(sequences); i++) {
+ seqs_len += strlen(svector_read_at(sequences, i));
+ }
+ char *const result = mh_xmalloc(seqs_len);
+ char *mp = result;
+ for (size_t i = 0; i < svector_size(sequences); i++) {
+ mp = stpcpy(mp, svector_read_at(sequences, i));
+ mp = stpcpy(mp, " ");
+ }
+ if (mp == result) {
+ stpcpy(result, "\n");
+ } else {
+ *(mp - 1) = '\n';
+ }
+ return result;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+/* copied from seq_read.c
+ * TODO put them back together
+ */
+
+#include "ssequal.h"
+#include "trimcpy.h"
+#include "m_atoi.h"
+#include "brkstring.h"
+#include "lock_file.h"
+
+/*
+ * static prototypes
+ */
+static int seq_init (struct msgs *, char *, char *);
+static void seq_private (struct msgs *);
+
+
+
+/*
+ * read folder's sequences file for public sequences
+ */
+
+typedef struct {
+ // outputs
+ bool done;
+ int failed_to_lock;
+ char name[NAMESZ], *value;
+
+ // inputs
+ struct msgs *mp;
+ int lockflag;
+
+ // internal state
+ char seqfile[PATH_MAX];
+ FILE *fp;
+ m_getfld_state_t gstate;
+ char valuebuf[NMH_BUFSIZ];
+} seq_public_state_t;
+static int
+seq_public_open(seq_public_state_t *const sps)
+{
+ /*
+ * If mh_seq == NULL or if *mh_seq == 0 (the user has defined
+ * the "mh-sequences" profile entry, but left it empty),
+ * then just return, and do not initialize any public sequences.
+ */
+ if (mh_seq == NULL || *mh_seq == 0) {
+ sps->done = true;
+ return OK;
+ }
+ sps->done = false;
+
+ /* get filename of sequence file */
+ snprintf(sps->seqfile, sizeof(sps->seqfile), "%s/%s", sps->mp->foldpath, mh_seq);
+ sps->fp = lkfopendata(sps->seqfile, sps->lockflag ? "r+" : "r", &sps->failed_to_lock);
+ if (sps->fp == NULL) {
+ sps->done = true;
+ return NOTOK;
+ }
+
+ /* Use m_getfld2 to scan sequence file */
+ sps->gstate = m_getfld_state_init(sps->fp);
+ return OK;
+}
+static void
+seq_public_next(seq_public_state_t *const sps)
+{
+ if (sps->done) {
+ adios(sps->seqfile, "attempt to keep reading after sequence file is done");
+ }
+ {
+ int state, fieldsz = sizeof sps->valuebuf;
+ switch (state = m_getfld2(&sps->gstate, sps->name, sps->valuebuf, &fieldsz)) {
+ case FLD:
+ case FLDPLUS:
+ if (state == FLDPLUS) {
+ char *cp = mh_xstrdup(sps->valuebuf);
+ while (state == FLDPLUS) {
+ fieldsz = sizeof sps->valuebuf;
+ state = m_getfld2(&sps->gstate, sps->name, sps->valuebuf, &fieldsz);
+ cp = add (sps->valuebuf, cp);
+ }
+ sps->value = trimcpy(cp);
+ free(cp);
+ } else {
+ sps->value = trimcpy(sps->valuebuf);
+ }
+ return;
+
+ case BODY:
+ lkfclosedata (sps->fp, sps->seqfile);
+ die("no blank lines are permitted in %s", sps->seqfile);
+ break;
+
+ case FILEEOF:
+ break;
+
+ default:
+ lkfclosedata (sps->fp, sps->seqfile);
+ die("%s is poorly formatted", sps->seqfile);
+ }
+ }
+ sps->done = true;
+ m_getfld_state_destroy (&sps->gstate);
+
+ if (sps->lockflag) {
+ sps->mp->seqhandle = sps->fp;
+ sps->mp->seqname = mh_xstrdup(sps->seqfile);
+ } else {
+ lkfclosedata (sps->fp, sps->seqfile);
+ }
+}
+
+
+/*
+ * Scan profile/context list for private sequences.
+ *
+ * We search the context list for all keys that look like
+ * "atr-seqname-folderpath", and add them as private sequences.
+ */
+
+static void
+seq_private (struct msgs *mp)
+{
+ int i, j, alen, plen;
+ char *cp;
+ struct node *np;
+
+ alen = LEN("atr-");
+ plen = strlen (mp->foldpath) + 1;
+
+ for (np = m_defs; np; np = np->n_next) {
+ if (ssequal ("atr-", np->n_name)
+ && (j = strlen (np->n_name) - plen) > alen
+ && *(np->n_name + j) == '-'
+ && strcmp (mp->foldpath, np->n_name + j + 1) == 0) {
+ cp = mh_xstrdup(np->n_name + alen);
+ *(cp + j - alen) = 0;
+ if ((i = seq_init (mp, cp, getcpy (np->n_field))) != -1)
+ make_seq_private (mp, i);
+ }
+ }
+}
+
+
+/*
+ * Split up the different message ranges at whitespace
+ */
+typedef struct {
+ bool done;
+ int start;
+ int end;
+ char *const *piece;
+ char *field;
+} parse_range_t;
+static void
+parse_range(parse_range_t *const state)
+{
+ if (state->done) {
+ die("attempt to keep reading after sequence range is done");
+ }
+ if (state->piece == NULL) {
+ state->piece = brkstring(state->field, " ", "\n");
+ state->done = *state->piece == NULL;
+ } else {
+ state->piece++;
+ }
+ if ((state->done = *state->piece == NULL)) {
+ return;
+ }
+ char *const dash_if_any = strchr(*state->piece, '-');
+ if (dash_if_any != NULL)
+ *dash_if_any = 0;
+ state->start = m_atoi (*state->piece);
+ if (state->start > 0) {
+ state->end = dash_if_any == NULL ? state->start : m_atoi(dash_if_any+1);
+ }
+}
+
+/*
+ * Add the name of sequence to the list of folder sequences.
+ * Then parse the list of message ranges for this
+ * sequence, and setup the various bit flags for each
+ * message in the sequence.
+ *
+ * Return internal index for the sequence if successful.
+ * Return -1 on error.
+ */
+
+static int
+seq_init (struct msgs *mp, char *name, char *field)
+{
+ unsigned int i;
+ int is_current;
+
+ /*
+ * Check if this is "cur" sequence,
+ * so we can do some special things.
+ */
+ is_current = !strcmp (current, name);
+
+ /*
+ * Search for this sequence name to see if we've seen
+ * it already. If we've seen this sequence before,
+ * then clear the bit for this sequence from all the
+ * messages in this folder.
+ */
+ for (i = 0; i < svector_size (mp->msgattrs); i++) {
+ if (!strcmp (svector_read_at (mp->msgattrs, i), name)) {
+ for (int j = mp->lowmsg; j <= mp->hghmsg; j++)
+ clear_sequence (mp, i, j);
+ break;
+ }
+ }
+
+ /*
+ * If we've already seen this sequence name, just free the
+ * name string. Else add it to the list of sequence names.
+ */
+ if (svector_read_at (mp->msgattrs, i)) {
+ free (name);
+ } else {
+ svector_push_back (mp->msgattrs, name);
+ }
+
+ /*
+ * Split up the different message ranges at whitespace
+ */
+ parse_range_t state;
+ state.done = false;
+ state.piece = NULL;
+ state.field = field;
+ for (parse_range(&state); !state.done; parse_range(&state)) {
+ if (state.start > 0) {
+ /*
+ * Keep mp->curmsg and "cur" sequence in synch. Unlike
+ * other sequences, this message doesn't need to exist.
+ * Think about the series of command (rmm; next) to
+ * understand why this can be the case. But if it does
+ * exist, we will still set the bit flag for it like
+ * other sequences.
+ */
+ if (is_current)
+ mp->curmsg = state.start;
+ /*
+ * We iterate through messages in this range
+ * and flip on bit for this sequence.
+ */
+ for (int j = state.start; j <= state.end; j++) {
+ if (j >= mp->lowmsg && j <= mp->hghmsg && does_exist(mp, j))
+ add_sequence (mp, i, j);
+ }
+ }
+ }
+
+ free (field); /* free string containing message ranges */
+ return i;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+/* copied from folder_read.c
+ * TODO put them back together
+
+ * The original folder_read uses readdir to find messages, but managed folders
+ * have a different source of truth known only to the folder manager.
+ * We never ask the folder manager for ALL messages. Instead, we pass it the
+ * user's message specifications and expect it to write all matching message
+ * numbers into the nmh-picked sequence.
+ */
+
+#include "m_maildir.h"
+
+/* We allocate the `mi' array 1024 elements at a time */
+#define NUMMSGS 1024
+
+/*
+ * 1) Create the folder/message structure
+ * 2) Read the directory (folder) and temporarily
+ * record the numbers of the messages we have seen.
+ * 3) Then allocate the array for message attributes and
+ * set the initial flags for all messages we've seen.
+ * 4) Read and initialize the sequence information.
+ */
+
+struct msgs *
+mh_managed_folder_read (char *in_name, int lockflag)
+{
+ struct msgs *mp;
+ struct bvector *v;
+ size_t i;
+ char *const name = m_mailpath (in_name);
+
+ /* Allocate the main structure for folder information */
+ NEW(mp);
+ clear_folder_flags (mp);
+ mp->foldpath = name;
+ mp->lowmsg = 0;
+ mp->hghmsg = 0;
+ mp->curmsg = 0;
+ mp->lowsel = 0;
+ mp->hghsel = 0;
+ mp->numsel = 0;
+ mp->nummsg = 0;
+ mp->seqhandle = NULL;
+ mp->seqname = NULL;
+
+ if (access (name, W_OK) == -1)
+ set_readonly (mp);
+
+ mp->msgattrs = svector_create (0);
+
+ // inlined fromseq_read
+ /*
+ * Initialize the list of sequence names. Go ahead and
+ * add the "cur" sequence to the list of sequences.
+ */
+ svector_push_back (mp->msgattrs, getcpy (current));
+ make_all_public (mp); /* initially, make all public */
+
+ seq_public_state_t sps;
+ sps.mp = mp;
+ sps.lockflag = lockflag;
+ if (seq_public_open(&sps) != OK) {
+ if (sps.failed_to_lock) {
+ advise (sps.seqfile, "failed to lock");
+ // TODO leaks!
+ return NULL;
+ }
+ perror("TODO is errno even useful or did lkfopendata clobber it?");
+ adios(sps.seqfile, "TODO pre-existing bug was to continue on all other failures");
+ // TODO leaks!
+ return NULL;
+ }
+
+ {
+ /*
+ * Allocate a temporary place to record the
+ * name of the messages in this folder.
+ */
+ int len = NUMMSGS;
+ int *mi = mh_xmalloc ((size_t) (len * sizeof(*mi)));
+
+ seq_public_next(&sps);
+ // TODO make nmh-picked configurable. if user already configured Previous-Sequence, use that?
+ if (!sps.done && strcmp(sps.name, "nmh-picked") == 0) {
+ parse_range_t state;
+ state.done = false;
+ state.piece = NULL;
+ state.field = sps.value;
+ for (parse_range(&state); !state.done; parse_range(&state)) {
+ if (state.start <= 0) {
+ adios(sps.seqfile, "folder manager put invalid message number %d into `nmh-picked` sequence", state.start);
+ }
+ if (state.end < state.start) {
+ adios(sps.seqfile, "folder manager put invalid message range %d-%d into `nmh-picked` sequence", state.start, state.end);
+ }
+ for (int msgnum = state.start; msgnum <= state.end; msgnum++) {
+ /*
+ * Check if we need to allocate more
+ * temporary elements for message names.
+ */
+ if (mp->nummsg >= len) {
+ len += NUMMSGS;
+ mi = mh_xrealloc (mi, (size_t) (len * sizeof(*mi)));
+ }
+
+ /* Check if this is the first message we've seen */
+ if (mp->nummsg == 0) {
+ mp->lowmsg = msgnum;
+ mp->hghmsg = msgnum;
+ } else {
+ /* Check if this is it the highest or lowest we've seen? */
+ if (msgnum < mp->lowmsg)
+ mp->lowmsg = msgnum;
+ if (msgnum > mp->hghmsg)
+ mp->hghmsg = msgnum;
+ }
+
+ /*
+ * Now increment count, and record message
+ * number in a temporary place for now.
+ */
+ mi[mp->nummsg++] = msgnum;
+
+ /* TODO Original readdir-based version would set this when
+ * encountering a file type it didn't know about. But this is
+ * only used by `folder -r` to determine whether this folder
+ * has subfolders to process.
+ * Instead we'll have `folder -r` ask the folder manager to
+ * enumerate subfolders. */
+ //set_other_files (mp);
+ }
+ }
+
+ free(sps.value);
+ seq_public_next(&sps);
+ }
+
+ mp->lowoff = max (mp->lowmsg, 1);
+
+ /* Go ahead and allocate space for 100 additional messages. */
+ mp->hghoff = mp->hghmsg + 100;
+
+ /* for testing, allocate minimal necessary space */
+ /* mp->hghoff = max (mp->hghmsg, 1); */
+
+ /*
+ * If for some reason hghoff < lowoff (like we got an integer overflow)
+ * the complain about this now.
+ */
+
+ if (mp->hghoff < mp->lowoff) {
+ die("Internal failure: high message limit < low message "
+ "limit; possible overflow?");
+ }
+
+ /*
+ * Allocate space for status of each message.
+ */
+ mp->num_msgstats = MSGSTATNUM (mp->lowoff, mp->hghoff);
+ mp->msgstats = mh_xmalloc (MSGSTATSIZE(mp));
+ for (i = 0, v = mp->msgstats; i < mp->num_msgstats; ++i, ++v) {
+ bvector_init(v);
+ }
+
+ /*
+ * Scan through the array of messages we've seen and
+ * setup the initial flags for those messages in the
+ * newly allocated mp->msgstats area.
+ */
+ for (int msgnum = 0; msgnum < mp->nummsg; msgnum++) {
+ set_exists (mp, mi[msgnum]);
+ }
+
+ free (mi); /* We don't need this anymore */
+ }
+
+ /*
+ * Read and initialize the sequence information.
+ */
+ // inlined seq_read
+/*
+ * Get the sequence information for this folder from
+ * .mh_sequences (or equivalent specified in .mh_profile)
+ * or context file (for private sequences).
+ */
+
+
+ /* If folder is empty, don't scan for sequence information */
+ if (mp->nummsg == 0) {
+ return mp;
+ }
+
+ /* Initialize the public sequences */
+ for (; !sps.done; seq_public_next(&sps)) {
+ seq_init(mp, mh_xstrdup(sps.name), sps.value);
+ }
+
+ /* Initialize the private sequences */
+ seq_private (mp);
+
+ return mp;
+}
--- /dev/null
+#include "h/mh.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "h/folder_manager_protocol.h"
+#include "h/utils.h"
+#include "sbr/error.h"
+#include "sbr/getcpy.h"
+#include "sbr/trimcpy.h"
+#include "sbr/m_getfld.h"
+
+// TODO replace with calling the Rust protocol implementation.
+
+void
+free_svector_and_elements(svector_t *svector)
+{
+ size_t i;
+ if (*svector == NULL) return;
+ size_t const len = svector_size(*svector);
+ for (i = 0; i < len; i++) {
+ free(svector_at(*svector, i));
+ }
+ svector_free(*svector);
+ *svector = NULL;
+}
+
+bool
+mh_folder_manager_protocol_read_message(svector_t message, mh_folder_manager_protocol_read_message_t *const rms)
+{
+ size_t len = 0, bytes_read = 0;
+ for (;;) {
+ int size = sizeof rms->value_buf;
+ int state = m_getfld2(&rms->getfld_ctx, rms->name, rms->value_buf, &size);
+ switch (state) {
+ case FLD:
+ case FLDPLUS: {
+ char *value, *trimmed;
+ if (state == FLD) {
+ value = trimcpy(rms->value_buf);
+ } else {
+ char *tmp = getcpy(rms->value_buf);
+ while (state == FLDPLUS) {
+ size = sizeof rms->value_buf;
+ state = m_getfld2(&rms->getfld_ctx, rms->name, rms->value_buf, &size);
+ tmp = add(rms->value_buf, tmp);
+ }
+ value = trimcpy(tmp);
+ free(tmp);
+ }
+ bytes_read += size;
+ trimmed = trimcpy(rms->name);
+
+ if (strcmp(trimmed, "len") == 0) {
+ if (len > 0) {
+ /* m_getfld2 already spews to stderr so why not. */
+ fprintf(stderr, "len component repeated\n");
+ return false;
+ }
+ errno = 0;
+ len = strtol(value, NULL, 10);
+ if (errno != 0) {
+ /* m_getfld2 already spews to stderr so why not. */
+ perror("len");
+ return false;
+ }
+ if (len == 0) {
+ /* m_getfld2 already spews to stderr so why not. */
+ fprintf(stderr, "len component repeated\n");
+ return false;
+ }
+ }
+
+ if (len == 0) {
+ /* m_getfld2 already spews to stderr so why not. */
+ fprintf(stderr, "len must be first not %s:%s\n", trimmed, value);
+ return false;
+ }
+
+ svector_push_back(message, trimmed);
+ svector_push_back(message, value);
+
+ if (bytes_read > len) {
+ /* m_getfld2 already spews to stderr so why not. */
+ fprintf(stderr, "we read %zu instead of %zu\n", bytes_read, len);
+ return false;
+ }
+ if (bytes_read == len) {
+ return true;
+ }
+ break;
+ }
+
+ case BODY:
+ fprintf(stderr, "should never see body: %s\n", rms->value_buf);
+ return false;
+
+ case FILEEOF:
+ if (len == 0) {
+ fprintf(stderr, "EOF before we saw anything? size=%d\n", size);
+ } else {
+ fprintf(stderr, "EOF with %zu bytes missing\n", len - bytes_read);
+ }
+ return false;
+
+ default:
+ die("unexpected m_getfld2 state %d\n", state);
+ }
+ }
+ die("UNREACHABLE");
+}
+
+typedef struct {
+ char b[16];
+ size_t len;
+} len_buf_t;
+static len_buf_t
+compute_len(size_t const partial_len)
+{
+ len_buf_t len_buf;
+ *len_buf.b = 0;
+ size_t len_len = 1;
+ for (;;) {
+ sprintf(len_buf.b, "%zu", partial_len + len_len);
+ size_t const new_len_len = strlen(len_buf.b);
+ if (new_len_len == len_len)
+ break;
+ len_len = new_len_len;
+ }
+ len_buf.len = len_len;
+ return len_buf;
+}
+
+bool
+mh_folder_manager_protocol_write_message(FILE *const fp, char const *const message, size_t partial_len)
+{
+ len_buf_t const len_buf = compute_len(partial_len + 5);
+
+ if (fputs("len:", fp) == EOF) {
+ perror("TODO fputs");
+ return false;
+ }
+ if (fwrite(len_buf.b, 1, len_buf.len, fp) < len_buf.len) {
+ perror("TODO fwrite");
+ return false;
+ }
+ if (fputs("\n", fp) == EOF) {
+ perror("TODO fputs");
+ return false;
+ }
+ if (fwrite(message, 1, partial_len, fp) < partial_len) {
+ perror("TODO fwrite");
+ return false;
+ }
+
+ if (fflush(fp) == EOF) {
+ perror("TODO fflush");
+ return false;
+ }
+
+ return true;
+}
--- /dev/null
+/* folder_operations.c -- mh_folder and associated operations.
+ *
+ * This code is Copyright (c) 2020, by the authors of nmh. See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+#include "h/mh.h"
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "h/done.h"
+#include "h/managed_folders.h"
+#include "h/folder.h"
+#include "h/utils.h"
+#include "sbr/brkstring.h"
+#include "sbr/context_find.h"
+#include "sbr/context_replace.h"
+#include "sbr/context_save.h"
+#include "sbr/error.h"
+#include "sbr/folder_free.h"
+#include "sbr/folder_read.h"
+#include "sbr/m_convert.h"
+#include "sbr/m_maildir.h"
+#include "sbr/m_name.h"
+#include "sbr/seq_getnum.h"
+#include "sbr/seq_save.h"
+#include "sbr/seq_setcur.h"
+#include "sbr/seq_setprev.h"
+#include "sbr/seq_setunseen.h"
+
+struct mh_folder {
+ char *name, *path;
+ struct msgs *mp;
+
+ mh_managed_folder *mmf;
+
+ // for scan
+ int num_unseen_seq;
+ ivector_t seqnum;
+};
+
+static mh_folder *
+new_mhf()
+{
+ mh_folder *mhf;
+ NEW(mhf);
+ mhf->name = mhf->path = NULL;
+ mhf->mp = NULL;
+ mhf->seqnum = ivector_create (0);
+ return mhf;
+}
+
+bool
+mh_folder_open(mh_folder **const result, char const *const name, mh_managed_folders *const mmfs)
+{
+ mh_folder *const mhf = *result = new_mhf();
+ mhf->name = mh_xstrdup(name);
+ mhf->path = mh_xstrdup(m_maildir(mhf->name));
+
+ mhf->mmf = mh_managed_folder_lookup(mmfs, name, strlen(name));
+
+ return true;
+}
+
+char const *
+mh_folder_get_err_string(const mh_folder *const mhf)
+{
+ if (mhf->mmf != NULL)
+ return mh_managed_folder_get_err_string(mhf->mmf);
+ adios(mhf->path, "mh_folder_get_err_string not implemented because everything just dies");
+ return NULL;
+}
+
+void
+mh_folder_close(mh_folder *const mhf)
+{
+ free(mhf->name);
+ free(mhf->path);
+ ivector_free(mhf->seqnum);
+ folder_free(mhf->mp);
+
+ free(mhf);
+}
+
+/* TODO Factor just a few common message selection routines out of all these
+ * after message selection has been factored out of all uip programs. */
+
+static void
+load_unseen_sequences_for_scan(mh_folder *const mhf)
+{
+ /*
+ * Get the sequence number for each sequence
+ * specified by Unseen-Sequence
+ */
+ char const *const cp = context_find (usequence);
+ if (cp != NULL && *cp != 0) {
+ char *const dp = mh_xstrdup(cp);
+ int i = 0;
+ for (char *const *ap = brkstring (dp, " ", "\n"); ap && *ap; i++, ap++) {
+ ivector_push_back (mhf->seqnum, seq_getnum (mhf->mp, *ap));
+ }
+ mhf->num_unseen_seq = i;
+ free(dp);
+ }
+}
+
+struct msgs *
+mh_managed_folder_read (char *, int);
+
+static struct msgs *
+managed_select(mh_folder *const mhf, struct msgs_array const msgs, int const need_unseen)
+{
+ mh_folder_manager_client *mfmc;
+ if (!mh_folder_manager_connect(&mfmc, mhf->mmf))
+ die("TODO: mh_folder_manager_connect");
+ if (!mh_folder_manager_pick(mfmc, mhf->name, msgs, need_unseen))
+ die("TODO mh_folder_manager_pick");
+ if (!mh_folder_manager_close(mfmc))
+ die("TODO mh_folder_manager_close");
+ struct msgs *mp = mh_managed_folder_read(mhf->name, 1);
+ if (mp == NULL)
+ adios(mhf->name, "TODO mh_managed_folder_read");
+ // See comments above mh_managed_folder_read for explanation.
+ if (!m_convert(mp, "all")) {
+ done(1);
+ }
+ return mp;
+}
+
+static struct msgs *
+managed_select_and_mark_seen(mh_folder *const mhf, struct msgs_array const msgs)
+{
+ mh_folder_manager_client *mfmc;
+ if (!mh_folder_manager_connect(&mfmc, mhf->mmf))
+ die("TODO: mh_folder_manager_connect");
+ svector_t sequences = svector_create (0);
+ // TODO Unseen-Sequence
+ svector_push_back(sequences, "unseen");
+ bool ok = mh_folder_manager_mark_del(mfmc, mhf->name, msgs, sequences);
+ svector_free(sequences);
+ if (!ok)
+ die("TODO mh_folder_manager_mark_del");
+ if (!mh_folder_manager_close(mfmc))
+ die("TODO mh_folder_manager_close");
+ struct msgs *mp = mh_managed_folder_read(mhf->name, 1);
+ if (mp == NULL)
+ adios(mhf->name, "TODO mh_managed_folder_read");
+ // See comments above mh_managed_folder_read for explanation.
+ if (!m_convert(mp, "all")) {
+ done(1);
+ }
+ return mp;
+}
+
+struct msgs const *
+mh_folder_select_and_chdir_for_scan(mh_folder *const mhf, struct msgs_array const msgs)
+{
+ if (chdir (mhf->path) == NOTOK)
+ adios (mhf->path, "unable to change directory to");
+
+ if (mhf->mmf == NULL) {
+ /* read folder and create message structure */
+ if (!(mhf->mp = folder_read (mhf->name, 1)))
+ die("unable to read folder %s", mhf->name);
+
+ /* check for empty folder */
+ if (mhf->mp->nummsg == 0)
+ die("no messages in %s", mhf->name);
+
+ /* parse all the message ranges/sequences and set SELECTED */
+ for (int i = 0; i < msgs.size; i++) {
+ if (!m_convert (mhf->mp, msgs.msgs[i]))
+ done(1);
+ }
+ } else {
+ mhf->mp = managed_select(mhf, msgs, /* need_unseen */ 1);
+ }
+
+ /* set the Previous-Sequence */
+ seq_setprev (mhf->mp);
+
+ /* update current folder */
+ context_replace (pfolder, mhf->name);
+ /* synchronize message sequences */
+ seq_save (mhf->mp);
+ /* save the context file */
+ context_save ();
+
+ // TODO Should be part of Open or Read?
+ load_unseen_sequences_for_scan(mhf);
+
+ return mhf->mp;
+}
+
+struct msgs const *
+mh_folder_select_and_chdir_and_mark_seen(struct msgs_array *const vec, mh_folder *const mhf, struct msgs_array const msgs)
+{
+ if (chdir (mhf->path) == NOTOK)
+ adios (mhf->path, "unable to change directory to");
+
+ if (mhf->mmf == NULL) {
+ /* read folder and create message structure */
+ if (!(mhf->mp = folder_read (mhf->name, 1)))
+ die("unable to read folder %s", mhf->name);
+
+ /* check for empty folder */
+ if (mhf->mp->nummsg == 0)
+ die("no messages in %s", mhf->name);
+
+ /* parse all the message ranges/sequences and set SELECTED */
+ for (int i = 0; i < msgs.size; i++) {
+ if (!m_convert (mhf->mp, msgs.msgs[i]))
+ done(1);
+ }
+
+ /*
+ * Set the SELECT_UNSEEN bit for all the SELECTED messages,
+ * since we will use that as a tag to know which messages
+ * to remove from the "unseen" sequence.
+ */
+ for (int msgnum = mhf->mp->lowsel; msgnum <= mhf->mp->hghsel; msgnum++) {
+ if (is_selected(mhf->mp, msgnum))
+ set_unseen (mhf->mp, msgnum);
+ }
+
+ seq_setprev (mhf->mp); /* set the Previous-Sequence */
+ seq_setunseen (mhf->mp, 1); /* unset the Unseen-Sequence */
+
+ // TODO Can we combine this with the previous loop? Tests passed...
+ for (int msgnum = mhf->mp->lowsel; msgnum <= mhf->mp->hghsel; msgnum++)
+ if (is_selected(mhf->mp, msgnum))
+ app_msgarg(vec, mh_xstrdup(m_name (msgnum)));
+
+ seq_setcur (mhf->mp, mhf->mp->hghsel); /* update current message */
+ seq_save (mhf->mp); /* synchronize sequences */
+ } else {
+ mhf->mp = managed_select_and_mark_seen(mhf, msgs);
+ for (int msgnum = mhf->mp->lowsel; msgnum <= mhf->mp->hghsel; msgnum++)
+ if (is_selected(mhf->mp, msgnum))
+ app_msgarg(vec, mh_xstrdup(m_name (msgnum)));
+ seq_setcur (mhf->mp, mhf->mp->hghsel); /* update current message */
+ seq_save (mhf->mp); /* synchronize sequences */
+ }
+
+ return mhf->mp;
+}
+
+bool
+mh_folder_message_is_unseen_for_scan(mh_folder *const mhf, int const msgnum)
+{
+ for (int i = 0; i < mhf->num_unseen_seq; i++) {
+ if (in_sequence(mhf->mp, ivector_at (mhf->seqnum, i), msgnum)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/* Mostly identical to scan version. Why can't this use msgs_array? */
+struct msgs const *
+mh_folder_select_and_chdir_for_send(mh_folder *const mhf, char *const *const msgs, size_t const msg_count)
+{
+ if (chdir (mhf->path) == NOTOK)
+ adios (mhf->path, "unable to change directory to");
+
+ if (mhf->mmf != NULL) {
+ // TODO Leaks msgs_array.msgs. Have send own the msgs_array.
+ struct msgs_array msgsa = { 0, 0, NULL };
+ for (size_t i = 0; i < msg_count; i++) {
+ app_msgarg(&msgsa, msgs[i]);
+ }
+ return managed_select(mhf, msgsa, /* need_unseen */ 0);
+ }
+
+ /* read folder and create message structure */
+ if (!(mhf->mp = folder_read (mhf->name, 1)))
+ die("unable to read folder %s", mhf->name);
+
+ /* check for empty folder */
+ if (mhf->mp->nummsg == 0)
+ die("no messages in %s", mhf->name);
+
+ /* parse all the message ranges/sequences and set SELECTED */
+ for (size_t i = 0; i < msg_count; i++) {
+ if (!m_convert (mhf->mp, msgs[i]))
+ done(1);
+ }
+ /* set the Previous-Sequence */
+ seq_setprev (mhf->mp);
+
+ return mhf->mp;
+}
*/
struct msgs *
-folder_read (char *name, int lockflag)
+folder_read (char *in_name, int lockflag)
{
int msgnum, len, *mi;
struct msgs *mp;
struct bvector *v;
size_t i;
- name = m_mailpath (name);
+ char *const name = m_mailpath (in_name);
if (!(dd = opendir (name))) {
free (name);
return NULL;
char **
getarguments (char *invo_name, int argc, char **argv, int check_context)
{
- char *cp = NULL, **ap = NULL, **bp = NULL, **arguments = NULL;
+ char *cp = NULL, **bp = NULL, **arguments = NULL;
+ char *const *ap = NULL;
int n = 0;
/*
ap = brkstring (cp, " ", "\n"); /* split string */
/* Count number of arguments split */
- bp = ap;
- while (*bp++)
+ char *const *counter = ap;
+ while (*counter++)
n++;
}
static int attr (struct msgs *, char *);
-int
-m_convert (struct msgs *mp, char *name)
+static int
+convert (struct msgs *mp, char *const name, const bool quiet)
{
int first, last, found, count, err;
char *bp, *cp;
case BADLST:
badlist:
- inform("bad message list %s", name);
+ if (!quiet) inform("bad message list %s", name);
break;
case BADNEW:
return 1;
}
+int
+m_convert (struct msgs *mp, char *name)
+{
+ return convert (mp, name, false);
+}
+
/*
* Convert the various message names to
* their numeric values.
--- /dev/null
+/* managed_folders.c
+ *
+ * This code is Copyright (c) 2020, by the authors of nmh. See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+#include "h/mh.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "h/managed_folders.h"
+#include "h/utils.h"
+#include "sbr/arglist.h"
+#include "sbr/concat.h"
+#include "sbr/context_find.h"
+#include "sbr/error.h"
+
+struct mh_managed_folders {
+ int status;
+ const char *error_message;
+
+ int count;
+ char **names;
+ mh_managed_folder **folders;
+};
+
+struct mh_managed_folder {
+ char const *name;
+ size_t name_len;
+ char *manager_path;
+ char *socket_path;
+ mh_managed_folders *mmfs;
+};
+
+void
+mh_managed_folder_set_err(mh_managed_folder *const mmf, const char *const error_message)
+{
+ if (error_message == NULL)
+ die("mh_managed_folder_set_err called with NULL error message");
+ mmf->mmfs->status = NOTOK;
+ mmf->mmfs->error_message = error_message;
+}
+
+bool
+mh_managed_folders_open(mh_managed_folders **const result)
+{
+ mh_managed_folders *mmfs;
+ NEW(mmfs);
+ *result = mmfs;
+
+ char *const managed_folders = context_find("Managed-Folders");
+ if (managed_folders == NULL) {
+ mmfs->count = 0;
+ } else {
+ // TODO argsplit is a poor choice here
+ char *unused;
+ mmfs->names = argsplit(managed_folders, &unused, &(mmfs->count));
+ mmfs->folders = mh_xmalloc(mmfs->count * sizeof *mmfs->folders);
+ for (int i = 0; i < mmfs->count; i++) {
+ NEW(mmfs->folders[i]);
+ mmfs->folders[i]->name_len = strlen(mmfs->names[i]);
+ char *key = concat(mmfs->names[i], "-manager", NULL);
+ // TODO free these
+ mmfs->folders[i]->name = mmfs->names[i];
+ mmfs->folders[i]->manager_path= context_find(key);
+ free(key);
+ key = concat(mmfs->names[i], "-socket", NULL);
+ mmfs->folders[i]->socket_path= context_find(key);
+ free(key);
+ mmfs->folders[i]->mmfs = mmfs;
+ }
+ free(unused);
+ }
+
+ mmfs->status = OK;
+ mmfs->error_message = NULL;
+ return true;
+}
+
+char const *
+mh_managed_folders_get_err_string(mh_managed_folders *const mmfs)
+{
+ if (mmfs->status == OK) {
+ return "mh_managed_folders_get_err_string called without error";
+ }
+ if (mmfs->error_message == NULL) {
+ return "TODO mh_managed_folders_get_err_string require non-OK and error_message";
+ }
+ return mmfs->error_message;
+}
+
+char const *
+mh_managed_folder_get_err_string(mh_managed_folder *const mmf)
+{
+ return mh_managed_folders_get_err_string(mmf->mmfs);
+}
+
+mh_managed_folder *
+mh_managed_folder_lookup(
+ mh_managed_folders const *const mmfs, char const *const name, size_t const name_len)
+{
+ for (int i = 0; i < mmfs->count; i++) {
+ mh_managed_folder *const folder = mmfs->folders[i];
+ if (folder->name_len <= name_len
+ && strncmp(mmfs->names[i], name, folder->name_len) == 0
+ && (folder->name_len == name_len
+ || name[folder->name_len] == '/')) {
+ return folder;
+ }
+ }
+ return NULL;
+}
+
+char const *
+mh_managed_folder_manager_path(const mh_managed_folder *mmf)
+{
+ return mmf->manager_path;
+}
+
+char const *
+mh_managed_folder_socket_path(const mh_managed_folder *mmf)
+{
+ return mmf->socket_path;
+}
+
+void
+mh_managed_folders_free(mh_managed_folders *const mmfs)
+{
+ int i;
+ for (i = 0; i < mmfs->count; i++) {
+ free(mmfs->names[i]);
+ free(mmfs->folders[i]);
+ }
+ free(mmfs->names);
+ free(mmfs->folders);
+ free(mmfs);
+}
+
+char const *
+mh_managed_folder_name(mh_managed_folder const *mmf)
+{
+ return mmf->name;
+}
nsc->ns_inbuflen -= size;
}
- return OK;
+ return retlen;
}
/*
* library when asked.
*/
-int
+static int
netsec_get_user(void *context, int id, const char **result,
unsigned int *len)
{
#ifdef CYRUS_SASL
sasl_security_properties_t secprops;
const char *chosen_mech;
- const unsigned char *saslbuf;
+ unsigned char *saslbuf;
unsigned char *outbuf;
unsigned int saslbuflen, outbuflen;
sasl_ssf_t *ssf;
*/
if (nsc->sasl_mech) {
- char **str, *mlist = getcpy(mechlist);
+ char *mlist = getcpy(mechlist);
+ char *const *const str = brkstring(mlist, " ", NULL);
int i;
- str = brkstring(mlist, " ", NULL);
-
for (i = 0; str[i] != NULL; i++) {
if (strcasecmp(nsc->sasl_mech, str[i]) == 0) {
break;
nsc->sasl_chosen_mech = mh_xstrdup(nsc->sasl_mech);
- if (mh_oauth_do_xoauth(nsc->ns_userid, nsc->oauth_service,
- &xoauth_client_res, &xoauth_client_res_len,
- nsc->ns_snoop ? stderr : NULL) != OK) {
- netsec_err(errstr, "Internal error: Unable to get OAuth2 "
- "bearer token");
- return NOTOK;
- }
+ mh_oauth_do_xoauth(nsc->ns_userid, nsc->oauth_service,
+ &xoauth_client_res, &xoauth_client_res_len,
+ nsc->ns_snoop ? stderr : NULL);
rc = nsc->sasl_proto_cb(NETSEC_SASL_START, xoauth_client_res,
- xoauth_client_res_len, NULL, 0,
+ xoauth_client_res_len, NULL, NULL,
nsc->sasl_proto_context, errstr);
free(xoauth_client_res);
* error.
*/
- rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0,
+ rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, NULL,
nsc->sasl_proto_context, errstr);
if (rc != OK) {
* NOT get a success message at this point.
*/
free(*errstr);
- nsc->sasl_proto_cb(NETSEC_SASL_WRITE, NULL, 0, NULL, 0,
+ nsc->sasl_proto_cb(NETSEC_SASL_WRITE, NULL, 0, NULL, NULL,
nsc->sasl_proto_context, NULL);
- rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0,
+ rc = nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, NULL,
nsc->sasl_proto_context, errstr);
if (rc == 0) {
netsec_err(errstr, "Unexpected success after OAuth failure!");
if (nsc->ns_snoop)
fprintf(stderr, "Chosen sasl mechanism: %s\n", chosen_mech);
- if (nsc->sasl_proto_cb(NETSEC_SASL_START, saslbuf, saslbuflen, NULL, 0,
+ if (nsc->sasl_proto_cb(NETSEC_SASL_START, saslbuf, saslbuflen, NULL, NULL,
nsc->sasl_proto_context, errstr) != OK)
return NOTOK;
if (nsc->sasl_proto_cb(NETSEC_SASL_READ, NULL, 0, &outbuf, &outbuflen,
nsc->sasl_proto_context, errstr) != OK) {
- nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0,
+ nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, NULL,
nsc->sasl_proto_context, NULL);
return NOTOK;
}
if (rc != SASL_OK && rc != SASL_CONTINUE) {
netsec_err(errstr, "SASL client negotiation failed: %s",
sasl_errdetail(nsc->sasl_conn));
- nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0,
+ nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, NULL,
nsc->sasl_proto_context, NULL);
return NOTOK;
}
if (nsc->sasl_proto_cb(NETSEC_SASL_WRITE, saslbuf, saslbuflen,
- NULL, 0, nsc->sasl_proto_context,
+ NULL, NULL, nsc->sasl_proto_context,
errstr) != OK) {
- nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, 0,
+ nsc->sasl_proto_cb(NETSEC_SASL_CANCEL, NULL, 0, NULL, NULL,
nsc->sasl_proto_context, NULL);
return NOTOK;
}
* from the server.
*/
- if (nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, 0,
+ if (nsc->sasl_proto_cb(NETSEC_SASL_FINISH, NULL, 0, NULL, NULL,
nsc->sasl_proto_context, errstr) != OK) {
/*
* At this point we can't really send an abort since the SASL dialog
static bool make_query_url(char *, size_t, CURL *, const char *, ...) ENDNULL;
static bool post(struct curl_ctx *, const char *, const char *);
-int
+void
mh_oauth_do_xoauth(const char *user, const char *svc, unsigned char **oauth_res,
size_t *oauth_res_len, FILE *log)
{
fp = lkfopendata(fn, "r+", &failed_to_lock);
if (fp == NULL) {
if (errno == ENOENT) {
- die("no credentials -- run mhlogin -saslmech xoauth2 -authservice %s", svc);
+ adios(fn, "credential file not found -- run mhlogin -saslmech xoauth2 -authservice %s", svc);
}
adios(fn, "failed to open");
}
mh_oauth_free(ctx);
*oauth_res = (unsigned char *) client_res;
-
- return OK;
}
static bool
static void intrser (int);
-char **
+char *const *
read_switch_multiword (const char *prompt, const struct swit *ansp)
{
int i;
SIGNAL_HANDLER istat = NULL;
- char *cp, **cpp;
+ char *cp;
if (sigsetjmp(sigenv, 1)) {
SIGNAL (SIGINT, istat);
print_sw (ALL, ansp, "", stdout);
continue;
}
- cpp = brkstring (ansbuf, " ", NULL);
+ char *const *cpp = brkstring (ansbuf, " ", NULL);
switch (smatch (*cpp, ansp)) {
case AMBIGSW:
ambigsw (*cpp, ansp);
* When one matches, return a pointer to an array of pointers to the words.
* Return NULL on EOF, interrupt, or other error.
*/
-char **read_switch_multiword(const char *, const struct swit *);
+char *const *read_switch_multiword(const char *, const struct swit *);
* getans, but with readline support
*/
-char **
+char *const *
read_switch_multiword_via_readline(char *prompt, struct swit *ansp)
{
- char *ans, **cpp;
+ char *ans;
initialize_readline();
rl_cmds = ansp;
ansbuf[sizeof(ansbuf) - 1] = '\0';
free(ans);
- cpp = brkstring(ansbuf, " ", NULL);
+ char *const *cpp = brkstring(ansbuf, " ", NULL);
switch (smatch(*cpp, ansp)) {
case AMBIGSW:
ambigsw(*cpp, ansp);
nmh_command_generator(const char *text, int state)
{
static int list_index, len;
- char *name, *p;
+ char *name;
char buf[256];
if (!state) {
list_index++;
strncpy(buf, name, sizeof(buf));
buf[sizeof(buf) - 1] = '\0';
- p = *brkstring(buf, " ", NULL);
+ const char *p = *brkstring(buf, " ", NULL);
if (strncmp(p, text, len) == 0)
return strdup(p);
}
* Same as read_switch_multiword but using readline(3) for input.
*/
#ifdef READLINE_SUPPORT
-char **read_switch_multiword_via_readline(char *, struct swit *);
+char *const *read_switch_multiword_via_readline(char *, struct swit *);
#endif /* READLINE_SUPPORT */
int
-refile (char **arg, char *file)
+refile (char *const *arg, char *const file)
{
pid_t pid;
int vecp;
* COPYRIGHT file in the root directory of the nmh distribution for
* complete copyright information. */
-int refile(char **, char *);
+int refile(char *const *, char *);
--- /dev/null
+[package]
+name = "nmh"
+# TODO derive from nmh version - or not; does it even matter? these are all internal crates
+version = "0.0.0"
+edition = "2018"
+
+[dependencies]
+libc = "0.2.78"
--- /dev/null
+#include "h/mh.h"
+#include "h/netsec.h"
+#include "h/utils.h"
+#include "sbr/base64.h"
+#include "sbr/client.h"
+#include "sbr/context_find.h"
+#include "sbr/dtime.h"
+#include "sbr/m_getfld.h"
--- /dev/null
+fn main() {
+ println!("cargo:rustc-flags=@LDFLAGS@");
+ println!("cargo:rustc-link-search=native=@abs_top_builddir@/sbr");
+
+ println!("cargo:rustc-link-lib=static=mh");
+ println!("cargo:rustc-link-lib=sasl2");
+ println!("cargo:rustc-link-lib=curl");
+ println!("cargo:rustc-link-lib=ssl");
+ println!("cargo:rustc-link-lib=crypto");
+}
--- /dev/null
+//! routines for converting to base64 (base64.h wrapper)
+
+use crate::sbr;
+use crate::str::{NString, UnsafeNString};
+
+pub fn encode_raw(bytes: &[u8]) -> NString {
+ let len = base64_size(bytes.len());
+ let mut b64_raw: UnsafeNString = NString::with_capacity(len).into_unsafe();
+ let b64_raw_unowned_ptr: *mut std::os::raw::c_uchar = b64_raw.mut_ptr();
+ unsafe {
+ sbr::writeBase64raw(
+ bytes.as_ptr(),
+ bytes.len() as sbr::size_t,
+ b64_raw_unowned_ptr,
+ );
+ }
+ unsafe { b64_raw.to_nstring(len) }.expect("writeBase64raw writes ASCII")
+}
+
+// copied from base64.h
+fn base64_size(x: usize) -> usize {
+ ((x + 2) / 3) * 4
+}
--- /dev/null
+//! connect to a server (client.h wrapper)
+
+use std::ffi::{CStr, CString};
+use std::os::raw::{c_char, c_int};
+
+use crate::sbr;
+use crate::str::NString;
+use crate::{Error, Result};
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Connector {
+ server: Option<CString>,
+ service: Option<CString>,
+}
+
+impl Connector {
+ pub fn new() -> Self {
+ Self {
+ server: None,
+ service: None,
+ }
+ }
+
+ /// Must be valid UTF-8. All other error-checking happens in [Connector::connect].
+ pub fn server(mut self, s: &str) -> Result<Self> {
+ self.server = Some(CString::new(s)?);
+ Ok(self)
+ }
+
+ /// Must be valid UTF-8. All other error-checking happens in [Connector::connect].
+ pub fn service(mut self, s: &str) -> Result<Self> {
+ self.service = Some(CString::new(s)?);
+ Ok(self)
+ }
+
+ /// Panics if called without setting server. Don't do that.
+ pub fn get_server(&self) -> &CStr {
+ self.server
+ .as_ref()
+ .expect("get_server called without server")
+ }
+
+ /// `connect(2)` to specified [Connector::server] and [Connector::service] and return the socket.
+ /// Panics if called without setting server and service
+ pub fn connect(&self) -> Result<std::os::unix::io::RawFd> {
+ let server_ptr = self
+ .server
+ .as_ref()
+ .expect("connect called without server")
+ .as_ptr() as *const c_char;
+ let service_ptr = self
+ .service
+ .as_ref()
+ .expect("connect called without service")
+ .as_ptr() as *const c_char;
+ // popsbr.c uses BUFSIZ which is 1024 (at least on FreeBSD 12.2).
+ let error_message_size: c_int = 1024;
+ let mut error_message = NString::with_capacity(error_message_size as usize);
+ error_message.push_ascii_str("client() provided no error message")?;
+ let mut error_message_raw = error_message.into_unsafe();
+ let socket = {
+ let error_message_raw_ptr = error_message_raw.mut_ptr() as *mut c_char;
+ unsafe {
+ sbr::client(
+ server_ptr,
+ service_ptr,
+ error_message_raw_ptr,
+ error_message_size,
+ 1,
+ )
+ }
+ };
+ if socket < 0 {
+ let error_message = unsafe { error_message_raw.to_nstring_sized_by_null_byte()? };
+ Err(Error::from(error_message))
+ } else {
+ Ok(socket)
+ }
+ }
+}
+
+impl Default for Connector {
+ fn default() -> Self {
+ Self::new()
+ }
+}
--- /dev/null
+use std::convert::TryFrom as _;
+
+use crate::str::{NString, NStringWithFree, OrString};
+
+/// [std::error::Error] that supports three kinds of error:
+/// - [Error::new] - a direct, base `Error` containing only an error message
+/// - [Error::wrap] - a wrapper around any other `Error` without additional information
+/// - [Error::describe] - a wrapper around any other `Error` with a description prefixed to the wrapped error's message
+///
+/// See below for examples and details on the debug and display formats of each kind.
+#[derive(Debug)]
+pub struct Error(Kind);
+
+#[derive(Debug)]
+enum Kind {
+ Direct(OrString),
+ Wrapper(Box<dyn std::error::Error>),
+ Described(String, Box<dyn std::error::Error>),
+}
+
+impl Error {
+ /// ```
+ /// assert_eq!("direct error", format!("{}", nmh::Error::new("direct error")));
+ /// assert_eq!(
+ /// "Error(Direct(String(\"direct error\")))",
+ /// format!("{:?}", nmh::Error::new("direct error"))
+ /// );
+ /// ```
+ pub fn new<T: Into<OrString>>(message: T) -> Self {
+ Self(Kind::Direct(message.into()))
+ }
+
+ /// ```
+ /// let error = "one".parse::<i32>().unwrap_err();
+ /// assert_eq!(
+ /// "invalid digit found in string",
+ /// format!("{}", nmh::Error::wrap(error.clone()))
+ /// );
+ /// assert_eq!(
+ /// "Error(Wrapper(ParseIntError { kind: InvalidDigit }))",
+ /// format!("{:?}", nmh::Error::wrap(error.clone()))
+ /// );
+ /// ```
+ pub fn wrap<E: 'static + std::error::Error>(e: E) -> Self {
+ Self(Kind::Wrapper(Box::new(e)))
+ }
+
+ /// ```
+ /// let error = "one".parse::<i32>().unwrap_err();
+ /// assert_eq!(
+ /// "parse(one): invalid digit found in string",
+ /// format!(
+ /// "{}",
+ /// nmh::Error::describe(error.clone(), format_args!("parse(one)"))
+ /// )
+ /// );
+ /// assert_eq!(
+ /// "Error(Described(\"parse(one): invalid digit found in string\", ParseIntError { kind: InvalidDigit }))",
+ /// format!(
+ /// "{:?}",
+ /// nmh::Error::describe(error.clone(), format_args!("parse(one)"))
+ /// )
+ /// );
+ /// ```
+ pub fn describe<E: 'static + std::error::Error>(e: E, fmt: std::fmt::Arguments) -> Self {
+ Self(Kind::Described(format!("{}: {}", fmt, e), Box::new(e)))
+ }
+}
+
+impl std::fmt::Display for Error {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match &self.0 {
+ Kind::Direct(message) => message.fmt(f),
+ Kind::Wrapper(source) => source.fmt(f),
+ Kind::Described(description, _) => description.fmt(f),
+ }
+ }
+}
+
+impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ match &self.0 {
+ Kind::Direct(_) => None,
+ Kind::Wrapper(source) => Some(source.as_ref()),
+ Kind::Described(_, source) => Some(source.as_ref()),
+ }
+ }
+}
+
+mod convert {
+ use super::*;
+
+ impl From<std::ffi::NulError> for Error {
+ fn from(e: std::ffi::NulError) -> Self {
+ Self::wrap(e)
+ }
+ }
+
+ impl From<std::num::TryFromIntError> for Error {
+ fn from(e: std::num::TryFromIntError) -> Self {
+ Self::wrap(e)
+ }
+ }
+
+ impl From<std::str::Utf8Error> for Error {
+ fn from(e: std::str::Utf8Error) -> Self {
+ Self::wrap(e)
+ }
+ }
+
+ impl From<NString> for Error {
+ fn from(ns: NString) -> Self {
+ Self(Kind::Direct(OrString::N(ns)))
+ }
+ }
+
+ impl From<NStringWithFree> for Error {
+ fn from(s: NStringWithFree) -> Self {
+ // TODO Expand OrString rather than copying.
+ match String::try_from(s) {
+ Err(e) => e,
+ Ok(s) => Self::new(s),
+ }
+ }
+ }
+
+ impl From<Error> for String {
+ fn from(e: Error) -> String {
+ e.to_string()
+ }
+ }
+}
--- /dev/null
+use std::path::{Path, PathBuf};
+
+use crate::{Error, Result};
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct Name(PathBuf);
+
+impl Name {
+ /// Construct from a [PathBuf] which MUST be a relative path encoded in UTF-8, else return [Error].
+ pub fn from_utf8(name: PathBuf) -> Result<Self> {
+ if name.is_absolute() {
+ Err(Error::new("folder name must be relative".to_owned()))
+ } else {
+ name.to_str()
+ .ok_or_else(|| Error::new("folder name must be in UTF-8"))?;
+ Ok(Self(name))
+ }
+ }
+
+ pub fn as_path(&self) -> &Path {
+ self.0.as_path()
+ }
+
+ pub fn to_str(&self) -> &str {
+ self.0.to_str().expect("checked in constructor")
+ }
+}
+
+impl std::fmt::Display for Name {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.display().fmt(f)
+ }
+}
--- /dev/null
+// TODO other splits have this problem too; generalize
+/// `"".split(' ')` returns [""] lol wut (ノಠ益ಠ)ノ彡┻━┻
+/// ```
+/// let empty = "";
+/// assert_eq!(vec![""], empty.split(' ').collect::<Vec<&str>>());
+///
+/// // ( ͡° ͜ ʖ ͡°)
+/// let expected_split: Vec<&str> = vec![];
+/// assert_eq!(expected_split, nmh::hacks::split(empty).collect::<Vec<&str>>());
+/// ```
+pub fn split<'a>(s: &'a str) -> Box<dyn std::iter::Iterator<Item = &str> + 'a> {
+ if s.is_empty() {
+ Box::new(std::iter::empty())
+ } else {
+ Box::new(s.split(' '))
+ }
+}
--- /dev/null
+pub mod base64;
+pub mod client;
+pub mod folder;
+pub mod hacks;
+pub mod message;
+pub mod netsec;
+pub mod profile;
+pub mod str;
+
+mod error;
+pub use error::Error;
+
+pub use netsec::netsec::Netsec;
+pub use profile::p::Profile;
+
+/// on error, nmh_init writes to stderr and this panics
+pub fn init(argv0: std::ffi::CString) -> Context {
+ let csptr = argv0.as_ptr();
+ // The only caller (imap-folder-manager) always wants the context.
+ let read_context = true;
+ // And also doesn't need to check version because it already happened.
+ let check_version = false;
+ if unsafe { sbr::nmh_init(csptr, read_context as i32, check_version as i32) } < 0 {
+ // nmh_init already spewed to stderr
+ panic!("nmh_init")
+ }
+ Context { _argv0: argv0 }
+}
+
+// Conceptually, things like netsec depend on this. But it's currently all
+// globals and you just have to call things in the right magical order or things
+// break. We use this to at least enforce things in Rust.
+pub struct Context {
+ _argv0: std::ffi::CString,
+}
+
+impl Context {
+ pub fn profile(&self) -> Profile {
+ Profile(self)
+ }
+}
+
+// Suppress all these warnings on bindgen's output.
+// TODO Yeesh. Are they going to clean this up?
+#[allow(dead_code)]
+#[allow(deref_nullptr)]
+#[allow(improper_ctypes)]
+#[allow(non_camel_case_types)]
+#[allow(non_snake_case)]
+#[allow(non_upper_case_globals)]
+mod bindgen;
+pub(crate) mod sbr;
+
+/// Return a reasonably-human-readable description of the current time.
+/// # Safety
+/// As safe as `dtimenow`.
+pub unsafe fn dtimenow() -> &'static std::ffi::CStr {
+ std::ffi::CStr::from_ptr(bindgen::dtimenow(0))
+}
+
+type Result<T> = std::result::Result<T, Error>;
--- /dev/null
+use std::num::NonZeroI32;
+
+mod set;
+pub use set::Set;
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
+pub struct Number(NonZeroI32);
+
+impl Number {
+ pub fn one() -> Self {
+ Self(NonZeroI32::new(1).expect("1 is not 0"))
+ }
+
+ pub fn increment(&mut self) {
+ self.0 = NonZeroI32::new(self.0.get() + 1).expect("non-zero + 1 is not 0");
+ }
+}
+
+impl std::fmt::Display for Number {
+ fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.0.fmt(formatter)
+ }
+}
+
+mod convert {
+ use super::*;
+
+ impl From<NonZeroI32> for Number {
+ fn from(n: NonZeroI32) -> Self {
+ Self(n)
+ }
+ }
+
+ impl From<Number> for i32 {
+ fn from(n: Number) -> i32 {
+ n.0.get()
+ }
+ }
+
+ impl From<Number> for NonZeroI32 {
+ fn from(n: Number) -> NonZeroI32 {
+ n.0
+ }
+ }
+
+ impl std::str::FromStr for Number {
+ type Err = crate::Error;
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let n: NonZeroI32 = s
+ .parse()
+ .map_err(|e| crate::Error::describe(e, format_args!("message number")))?;
+ if n.get() < 0 {
+ Err(crate::Error::new(format!("illegal message number {}", n)))
+ } else {
+ Ok(Number::from(n))
+ }
+ }
+ }
+}
--- /dev/null
+// TODO wrap sbr structures
+
+#[derive(Clone)]
+pub struct Set(Has);
+
+impl Set {
+ pub fn new() -> Self {
+ Self(Has::Set(std::collections::BTreeSet::new()))
+ }
+
+ pub fn all() -> Self {
+ Self(Has::All)
+ }
+
+ pub fn append(&mut self, other: &mut Self) {
+ if let Has::Set(set) = &mut self.0 {
+ match &mut other.0 {
+ Has::All => self.0 = Has::All,
+ Has::Set(other_set) => set.append(other_set),
+ }
+ }
+ }
+
+ pub fn insert(&mut self, msgnum: i32) -> bool {
+ match &mut self.0 {
+ Has::All => false,
+ Has::Set(set) => set.insert(msgnum),
+ }
+ }
+
+ pub fn remove(&mut self, msgnum: i32) -> bool {
+ match &mut self.0 {
+ Has::All => todo!(),
+ Has::Set(set) => set.remove(&msgnum),
+ }
+ }
+
+ pub fn clone_without(&self, other: &Self) -> Self {
+ match &self.0 {
+ Has::All => todo!(),
+ Has::Set(set) => match &other.0 {
+ Has::All => Self::new(),
+ Has::Set(other_set) => Self(Has::Set(set.difference(other_set).cloned().collect())),
+ },
+ }
+ }
+
+ pub fn ends(&self) -> Option<(i32, i32)> {
+ match &self.0 {
+ Has::All => Some((1, i32::MAX)),
+ Has::Set(set) => {
+ let mut i = set.iter();
+ match i.next() {
+ None => None,
+ Some(beg) => match i.next_back() {
+ None => Some((*beg, *beg)),
+ Some(end) => Some((*beg, *end)),
+ },
+ }
+ }
+ }
+ }
+
+ pub fn contains(&self, msgnum: i32) -> bool {
+ match &self.0 {
+ Has::All => true,
+ Has::Set(set) => set.contains(&msgnum),
+ }
+ }
+
+ pub fn is_empty(&self) -> bool {
+ match &self.0 {
+ Has::All => false,
+ Has::Set(set) => set.is_empty(),
+ }
+ }
+}
+
+impl std::fmt::Display for Set {
+ fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
+ match &self.0 {
+ Has::All => write!(f, "1-{}", i32::MAX),
+ Has::Set(set) => {
+ let mut space = "";
+ let (mut beg, mut end) = (-1, -1);
+ for msgnum in set.iter() {
+ if beg == -1 && end == -1 {
+ beg = *msgnum;
+ } else if *msgnum != end + 1 {
+ assert!(end > 0);
+ if beg == end {
+ write!(f, "{}{}", space, beg)?
+ } else {
+ write!(f, "{}{}-{}", space, beg, end)?
+ }
+ space = " ";
+ beg = *msgnum;
+ }
+ end = *msgnum;
+ }
+ if beg > 0 {
+ if beg == end {
+ write!(f, "{}{}", space, beg)
+ } else {
+ write!(f, "{}{}-{}", space, beg, end)
+ }
+ } else {
+ Ok(())
+ }
+ }
+ }
+ }
+}
+
+#[derive(Clone)]
+enum Has {
+ All,
+ Set(std::collections::BTreeSet<i32>),
+}
--- /dev/null
+//! network-security library routines (netsec.h wrapper)
+//!
+//! This is a wrapper around netsec, a common set of routines to handle network security for things
+//! like SASL and OpenSSL. See netsec.h for more information.
+//!
+//! Start with [crate::Netsec::builder].
+//!
+//! # Examples
+//!
+//! ```ignore
+//! use nmh::{client, netsec, Netsec};
+//!
+//! let mut builder: netsec::Builder = Netsec::builder()
+//! .host(&std::ffi::CString::new("hostname")?);
+//! let mut connector: netsec::tls::Connector = builder.connect(client::Connector::new().server("...")?.build());
+//! let mut authenticator: netsec::sasl::Authenticator = connector.negotiate_tls()?;
+//! let mut nsc: Netsec = authenticator.negotiate_sasl(&context, &mut my_callback)?;
+//! ```
+
+mod build;
+pub(crate) mod netsec;
+pub mod sasl;
+pub mod tls;
+
+pub use build::Builder;
+
+type ErrorString = crate::str::NStringWithFree;
+
+fn empty_cstring() -> std::ffi::CString {
+ std::ffi::CString::new("").expect("null byte in empty string")
+}
--- /dev/null
+use std::convert::TryInto;
+use std::ffi::{CStr, CString};
+use std::os::raw::c_int;
+
+use crate::{sbr, Result};
+
+use super::*;
+
+// We could use state transitions to prevent callers from bugs such as
+// attempting to negotiate TLS without first setting a host, but this is not
+// some published API with goals of stability, but internal, so we know the
+// cost of Holding It Wrong is not a subtle crash mid-operation, but merely an
+// obvious crash at startup.
+
+/// This is fragile.
+#[derive(Debug)]
+pub struct Builder {
+ pub(super) nsc: crate::Netsec,
+ pub(super) skip_cert_checks: bool,
+ pub(super) sasl: sasl::AuthenticatorBuilder,
+}
+
+impl Builder {
+ /// Must be valid UTF-8. All other error-checking happens after [Builder::connect].
+ pub fn host(self, host: &CStr) -> Self {
+ // The CString doesn't need to live past this scope because netsec_set_hostname makes its own copy.
+ let csptr = host.as_ptr();
+ unsafe {
+ sbr::netsec_set_hostname(self.nsc.nsc, csptr);
+ }
+ self
+ }
+
+ /// `timeout` must be representable as a 32-bit integer of seconds.
+ pub fn timeout(self, timeout: std::time::Duration) -> Result<Self> {
+ let timeout_c: c_int = timeout.as_secs().try_into()?;
+ unsafe { sbr::netsec_set_timeout(self.nsc.nsc, timeout_c) }
+ Ok(self)
+ }
+
+ /// Enable logging of traffic to the standard error.
+ pub fn snoop(self) -> Self {
+ unsafe { sbr::netsec_set_snoop(self.nsc.nsc, 1) }
+ self
+ }
+
+ /// I wouldn't.
+ pub fn skip_certificate_checks(mut self) -> Self {
+ self.skip_cert_checks = true;
+ self
+ }
+
+ /// Must be valid UTF-8. All other error-checking happens after [crate::netsec::sasl::Authenticator::negotiate_sasl].
+ pub fn sasl_service(mut self, value: &str) -> Result<Self> {
+ self.sasl.service = CString::new(value)?;
+ Ok(self)
+ }
+
+ /// Must be valid UTF-8. All other error-checking happens after [crate::netsec::sasl::Authenticator::negotiate_sasl].
+ pub fn sasl_userid(self, userid: &str) -> Result<Self> {
+ // The CString doesn't need to live past this scope because netsec_set_userid makes its own copy.
+ let cs = CString::new(userid)?;
+ let csptr = cs.as_ptr();
+ unsafe {
+ sbr::netsec_set_userid(self.nsc.nsc, csptr);
+ }
+ Ok(self)
+ }
+
+ /// Must be valid UTF-8. All other error-checking happens after [crate::netsec::sasl::Authenticator::negotiate_sasl].
+ pub fn sasl_prefer_mechanism(&mut self, value: &str) -> Result<()> {
+ self.sasl.preferred_mech = CString::new(value)?;
+ Ok(())
+ }
+
+ /// Must be valid UTF-8. All other error-checking happens after [crate::netsec::sasl::Authenticator::negotiate_sasl].
+ pub fn sasl_oauth_service(&mut self, value: &str) -> Result<()> {
+ // The CString doesn't need to live past this scope because netsec_set_userid makes its own copy.
+ let cs = CString::new(value)?;
+ let csptr = cs.as_ptr();
+ let status = unsafe { sbr::netsec_set_oauth_service(self.nsc.nsc, csptr) };
+ if status < 0 {
+ todo!("nmh not built with OAuth support");
+ }
+ Ok(())
+ }
+
+ /// `connect(2)` via the [crate::client::Connector] and return
+ /// [tls::Connector] for negotiating TLS.
+ pub fn connect(self, connector: crate::client::Connector) -> Result<tls::Connector> {
+ let socket: std::os::unix::io::RawFd = connector.connect()?;
+ unsafe { sbr::netsec_set_fd(self.nsc.nsc, socket, socket) }
+ Ok(tls::Connector { builder: self })
+ }
+
+ pub(super) fn new() -> Self {
+ Builder {
+ nsc: crate::Netsec::new(),
+ skip_cert_checks: false,
+ sasl: sasl::AuthenticatorBuilder {
+ preferred_mech: empty_cstring(),
+ service: empty_cstring(),
+ },
+ }
+ }
+}
--- /dev/null
+use std::os::raw::{c_char, c_int, c_void};
+
+use crate::sbr;
+
+use super::{Builder, ErrorString};
+
+#[derive(Debug)]
+pub struct Netsec {
+ pub(super) nsc: *mut sbr::netsec_context,
+ last_write_ends_with_crlf: bool,
+}
+
+impl Netsec {
+ pub fn builder() -> Builder {
+ Builder::new()
+ }
+
+ /// Safe due to mutable borrow: you can't call this a second time and
+ /// invalidate a previously stored &str reference.
+ pub fn read_line(&mut self) -> crate::Result<&mut str> {
+ let as_char_ptr: *mut c_char = self.read_line_raw()?;
+ let as_u8_ptr = as_char_ptr as *mut u8;
+ // TODO underlying netsec knows len and should return it
+ let as_u8_slice: &mut [u8] =
+ unsafe { std::slice::from_raw_parts_mut(as_u8_ptr, libc::strlen(as_char_ptr)) };
+ // Skip the valid UTF-8 enforcement, as we're going to instead enforce valid ASCII.
+ let as_str: &mut str = unsafe { std::str::from_utf8_unchecked_mut(as_u8_slice) };
+ if !as_str.is_ascii() {
+ Err(crate::Error::new(format!("not ASCII: {}", as_str)))
+ } else {
+ Ok(as_str)
+ }
+ }
+
+ pub fn read(&mut self, buffer: &mut [u8]) -> crate::Result<usize> {
+ let mut errstr = ErrorString::new();
+ let bytes_read: sbr::ssize_t = {
+ let buffer_c = buffer as *mut _ as *mut c_void;
+ let buffer_len = buffer.len() as sbr::size_t;
+ unsafe { sbr::netsec_read(self.nsc, buffer_c, buffer_len, errstr.pointer()) }
+ };
+ if bytes_read < 0 {
+ return Err(errstr.into());
+ }
+ Ok(bytes_read as usize)
+ }
+
+ pub fn write(&mut self, data: &[u8]) -> crate::Result<()> {
+ self.last_write_ends_with_crlf = data.ends_with(b"\r\n");
+ let mut errstr = ErrorString::new();
+ let status = unsafe {
+ sbr::netsec_write(
+ self.nsc,
+ data.as_ptr() as *const c_void,
+ data.len() as sbr::size_t,
+ errstr.pointer(),
+ )
+ };
+ if status != 0 {
+ Err(errstr.into())
+ } else {
+ Ok(())
+ }
+ }
+
+ // TODO Tried to do this cleanly but the base64-decoding in netsec snoop is all tangled up.
+ // There's this generic callback mechanism but it's actually only used for base64-decoding.
+ // The snoop only happens in flush so we have to flush before clearing the decoding snoop callback.
+ // The snoop only happens anyway if there's a CRLF in the buffer.
+ // The "offset at which base64 data starts" is also computed from the start of a line.
+ pub fn write_with_base64_at_offset_then_crlf_then_flush(
+ &mut self,
+ line: &str,
+ mut offset: c_int,
+ ) -> crate::Result<()> {
+ assert!(line.ends_with("\r\n"));
+ let offset_c = &mut offset as *mut _ as *mut c_void;
+ unsafe {
+ sbr::netsec_set_snoop_callback(self.nsc, SNOOP_CALLBACK, offset_c);
+ }
+ // Hold onto the success or failure while we clear the callback.
+ let result = self.write(line.as_bytes()).and_then(|_| self.flush());
+ unsafe {
+ sbr::netsec_set_snoop_callback(self.nsc, None, std::ptr::null_mut());
+ }
+ result
+ }
+
+ pub fn flush(&mut self) -> crate::Result<()> {
+ if !self.last_write_ends_with_crlf {
+ // I reliably triggered a runaway memory allocation when testing
+ // refile -file on Gandi, but only on Gandi. Why?
+ // TODO Come up with reliable test and fix the underlying bug.
+ todo!("flush may call snoop which may crash if the buffer doesn't end with CRLF");
+ }
+
+ let mut errstr = ErrorString::new();
+ let status = { unsafe { sbr::netsec_flush(self.nsc, errstr.pointer()) } };
+ if status != 0 {
+ return Err(errstr.into());
+ }
+ Ok(())
+ }
+
+ pub fn write_and_flush(&mut self, data: &[u8]) -> crate::Result<()> {
+ self.write(data)?;
+ self.flush()
+ }
+
+ pub(super) fn new() -> Self {
+ Self {
+ nsc: unsafe { sbr::netsec_init() },
+ last_write_ends_with_crlf: false,
+ }
+ }
+
+ fn read_line_raw(&mut self) -> crate::Result<*mut c_char> {
+ let mut len: sbr::size_t = 0;
+ let mut errstr = ErrorString::new();
+ let line: *mut c_char = {
+ let len_c = &mut len as *mut _;
+ unsafe { sbr::netsec_readline(self.nsc, len_c, errstr.pointer()) }
+ };
+ if line.is_null() {
+ return Err(errstr.into());
+ }
+ Ok(line)
+ }
+}
+
+impl Drop for Netsec {
+ fn drop(&mut self) {
+ unsafe {
+ sbr::netsec_shutdown(self.nsc);
+ }
+ }
+}
+
+const SNOOP_CALLBACK: sbr::netsec_snoop_callback = Some(sbr::netsec_b64_snoop_decoder);
--- /dev/null
+use std::ffi::{CStr, CString};
+use std::os::raw::{c_char, c_int, c_uchar, c_uint, c_void};
+
+use crate::{sbr, Netsec};
+
+use super::*;
+
+pub type Result<T, E> = std::result::Result<T, Error<E>>;
+pub enum Error<E> {
+ /// an error you returned from a [Callback] method
+ Callback(E),
+ /// an error [Authenticator::negotiate_sasl] encountered
+ Internal(crate::Error),
+}
+
+impl<E> From<crate::Error> for Error<E> {
+ fn from(e: crate::Error) -> Self {
+ Error::Internal(e)
+ }
+}
+
+impl From<Error<crate::Error>> for crate::Error {
+ fn from(e: Error<crate::Error>) -> Self {
+ match e {
+ Error::Internal(e) => e,
+ Error::Callback(e) => e,
+ }
+ }
+}
+
+/// Reading netsec.h is required at this point.
+///
+/// You can pass errors back out to yourself by returning [Error::Callback] from any of these
+/// methods and [Authenticator::negotiate_sasl] will return it to you.
+pub trait Callback<E> {
+ /// See `NETSEC_SASL_START`.
+ fn start(
+ &mut self,
+ nsc: &mut Netsec,
+ selected_mechanism: &str,
+ message: &[u8],
+ ) -> Result<(), E>;
+ /// See `NETSEC_SASL_READ`.
+ fn read(&mut self, nsc: &mut Netsec) -> Result<Vec<u8>, E>;
+ /// See `NETSEC_SASL_WRITE`.
+ fn write(&mut self, nsc: &mut Netsec, mesage: &[u8]) -> Result<(), E>;
+ /// See `NETSEC_SASL_FINISH`.
+ fn finish(&mut self, nsc: &mut Netsec) -> Result<(), E>;
+ /// See `NETSEC_SASL_CANCEL`.
+ fn cancel(&mut self) -> Result<(), E>;
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Ord, PartialOrd)]
+pub enum Mechanism {
+ Plain,
+ // TODO replace with OAuthBearer
+ XOAuth2,
+}
+
+impl std::fmt::Display for Mechanism {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(match self {
+ Self::Plain => "PLAIN",
+ Self::XOAuth2 => "XOAUTH2",
+ })
+ }
+}
+
+/// Use in conjunction with your [Callback] impl to negotiate authentication with SASL.
+#[derive(Debug)]
+pub struct Authenticator {
+ nsc: Netsec,
+ preferred_mech: CString,
+ server_mechlist: CString,
+ service: CString,
+}
+
+impl Authenticator {
+ /// Return a line of text from the server. Useful with [Authenticator::write_and_flush] for SASL.
+ pub fn read_line(&mut self) -> crate::Result<&mut str> {
+ self.nsc.read_line()
+ }
+
+ /// Return a line of text from the server. Useful with [Authenticator::read_line] for SASL.
+ pub fn write_and_flush(&mut self, data: &[u8]) -> crate::Result<()> {
+ self.nsc.write_and_flush(data)
+ }
+
+ pub fn server_supported_sasl_mechanisms<T: std::iter::Iterator<Item = Mechanism>>(
+ mut self,
+ value: T,
+ ) -> Self {
+ self.server_mechlist = CString::new(
+ value
+ .map(|mech| mech.to_string())
+ .collect::<Vec<_>>()
+ .join(" "),
+ )
+ .expect("sasl::Mechanism::to_string never to return null bytes");
+ self
+ }
+
+ /// Drive [Callback] to negotiate SASL.
+ /// If any of your [Callback] methods return [Error::Callback], return that error to you.
+ pub fn negotiate_sasl<E, T: Callback<E>>(
+ mut self,
+ // Used to lookup credentials, but under the hood, through globals.
+ _nmh: &crate::Context,
+ sasl_callback: &mut T,
+ ) -> Result<Netsec, E> {
+ let nsc = self.nsc.nsc;
+ let mut callback_data = CallbackData {
+ netsec: &mut self.nsc,
+ callback: sasl_callback,
+ error: None,
+ };
+ let mut errstr = ErrorString::new();
+ let mut status = {
+ let service = self.service.as_ptr();
+ let preferred_mech = if self.preferred_mech.as_bytes().is_empty() {
+ std::ptr::null()
+ } else {
+ self.preferred_mech.as_ptr()
+ };
+ let context = &mut callback_data as *mut _ as *mut c_void;
+ unsafe {
+ sbr::netsec_set_sasl_params(
+ nsc,
+ service,
+ preferred_mech,
+ Some(netsec_sasl_callback::<E, T>),
+ context,
+ errstr.pointer(),
+ )
+ }
+ };
+ if status != 0 {
+ return Err(Error::Internal(errstr.into()));
+ }
+ let mut errstr = ErrorString::new();
+ status = unsafe {
+ sbr::netsec_negotiate_sasl(nsc, self.server_mechlist.as_ptr(), errstr.pointer())
+ };
+ if let Some(e) = callback_data.error {
+ Err(e)
+ } else if status != 0 {
+ Err(Error::Internal(errstr.into()))
+ } else {
+ Ok(self.nsc)
+ }
+ }
+
+ pub fn skip_sasl(self) -> Netsec {
+ self.nsc
+ }
+}
+
+#[derive(Debug)]
+pub(super) struct AuthenticatorBuilder {
+ pub(super) preferred_mech: CString,
+ pub(super) service: CString,
+}
+
+impl AuthenticatorBuilder {
+ pub fn build(self, nsc: Netsec) -> Authenticator {
+ Authenticator {
+ nsc,
+ preferred_mech: self.preferred_mech,
+ server_mechlist: empty_cstring(),
+ service: self.service,
+ }
+ }
+}
+
+struct CallbackData<'a, E, T: Callback<E>> {
+ netsec: &'a mut Netsec,
+ callback: &'a mut T,
+ error: Option<Error<E>>,
+}
+
+unsafe extern "C" fn netsec_sasl_callback<E, T: Callback<E>>(
+ mtype: sbr::sasl_message_type,
+ indata: *const c_uchar,
+ indatasize: c_uint,
+ outdata: *mut *mut c_uchar,
+ outdatasize: *mut c_uint,
+ context: *mut c_void,
+ _: *mut *mut c_char,
+) -> c_int {
+ let nsc_callback = &mut *(context as *mut CallbackData<E, T>);
+ match dispatch_netsec_sasl_callback(
+ nsc_callback.netsec,
+ nsc_callback.callback,
+ mtype,
+ indata,
+ indatasize,
+ outdata,
+ outdatasize,
+ ) {
+ Err(e) => {
+ nsc_callback.error = Some(e);
+ -1
+ }
+ _ => 0,
+ }
+}
+
+fn u8_slice_from_cuchar_cuint<'a>(data: *const c_uchar, len: c_uint) -> &'a [u8] {
+ if len > 0 {
+ let len_usize = len as usize;
+ unsafe { std::slice::from_raw_parts(data, len_usize) }
+ } else {
+ &[]
+ }
+}
+
+fn dispatch_netsec_sasl_callback<E, T: Callback<E>>(
+ nsc: &mut Netsec,
+ callback: &mut T,
+ mtype: sbr::sasl_message_type,
+ indata: *const c_uchar,
+ indatasize: c_uint,
+ _outdata: *mut *mut c_uchar,
+ _outdatasize: *mut c_uint,
+) -> Result<(), E> {
+ match mtype {
+ sbr::sasl_message_type_NETSEC_SASL_START => {
+ let cstr = unsafe { CStr::from_ptr(sbr::netsec_get_sasl_mechanism(nsc.nsc)) };
+ let selected_mech: &str = std::str::from_utf8(cstr.to_bytes())
+ .expect("SASL mechanism never to return null bytes");
+ callback.start(
+ nsc,
+ selected_mech,
+ u8_slice_from_cuchar_cuint(indata, indatasize),
+ )
+ }
+ sbr::sasl_message_type_NETSEC_SASL_READ => {
+ todo!("NETSEC_SASL_READ")
+ // let result: Result<Vec<u8>, E> = callback.read(nsc);
+ // match result {
+ // Ok(mut bytes) => {
+ // let len_c: c_uint = bytes.len().try_into()?;
+ // let bytes_ptr = bytes.as_mut_ptr();
+ // unsafe {
+ // *outdatasize = len_c;
+ // *outdata = bytes_ptr;
+ // }
+ // Ok(())
+ // }
+ // Err(e) => Err(e),
+ // }
+ }
+ sbr::sasl_message_type_NETSEC_SASL_WRITE => {
+ todo!("NETSEC_SASL_WRITE")
+ // callback.write(nsc, u8_slice_from_cuchar_cuint(indata, indatasize))
+ }
+ sbr::sasl_message_type_NETSEC_SASL_FINISH => callback.finish(nsc),
+ sbr::sasl_message_type_NETSEC_SASL_CANCEL => todo!("NETSEC_SASL_CANCEL"),
+ _ => Err(Error::Internal(crate::Error::new(format!(
+ "illegal sasl_message_type {}",
+ mtype,
+ )))),
+ }
+}
--- /dev/null
+use crate::sbr;
+
+use super::*;
+
+#[derive(Debug)]
+pub struct Connector {
+ pub(super) builder: Builder,
+}
+
+impl Connector {
+ /// Return a line of text from the server. Useful with [Connector::write_and_flush] for STARTTLS.
+ pub fn read_line(&mut self) -> crate::Result<&mut str> {
+ self.builder.nsc.read_line()
+ }
+
+ /// Return a line of text from the server. Useful with [Connector::read_line] for STARTTLS.
+ pub fn write_and_flush(&mut self, data: &[u8]) -> crate::Result<()> {
+ self.builder.nsc.write_and_flush(data)
+ }
+
+ /// I wouldn't.
+ pub fn skip_tls(self) -> sasl::Authenticator {
+ self.builder.sasl.build(self.builder.nsc)
+ }
+
+ /// Negotiate TLS and prepare for authentication.
+ pub fn negotiate_tls(self) -> crate::Result<sasl::Authenticator> {
+ let mut errstr = ErrorString::new();
+ let mut status = {
+ unsafe {
+ sbr::netsec_set_tls(
+ self.builder.nsc.nsc,
+ 1,
+ if self.builder.skip_cert_checks { 1 } else { 0 },
+ errstr.pointer(),
+ )
+ }
+ };
+ if status != 0 {
+ return Err(errstr.into());
+ }
+ let mut errstr = ErrorString::new();
+ status = unsafe { sbr::netsec_negotiate_tls(self.builder.nsc.nsc, errstr.pointer()) };
+ if status != 0 {
+ return Err(errstr.into());
+ }
+ Ok(self.builder.sasl.build(self.builder.nsc))
+ }
+}
--- /dev/null
+//! facilities related to [crate::Profile]
+
+use std::os::raw::c_char;
+use std::path::Path;
+
+use crate::sbr;
+
+pub(crate) mod p {
+ use super::*;
+ pub struct Profile<'a>(pub(crate) &'a crate::Context);
+ impl<'a> Profile<'a> {
+ /// Return Some if the profile has a value for `name`, else None. Panic on any error.
+ pub fn get(&self, name: &str) -> Option<&str> {
+ get(name)
+ }
+
+ /// Return Ok if the profile has a value for `name`, else Err. Panic on error.
+ pub fn get_or_err(&self, name: &str) -> Result<&str, String> {
+ get_or_err(name)
+ }
+
+ /// Return view into the profile that only exposes components whose names begin with `prefix`.
+ pub fn prefixed(&self, prefix: &'a str) -> Prefixed<'a> {
+ Prefixed { prefix }
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ // common parameters
+ // See mh-profile(5).
+
+ pub fn path(&self) -> Result<&Path, String> {
+ self.get_or_err("path").map(Path::new)
+ }
+
+ pub fn previous_sequence(&self) -> Result<&Path, String> {
+ self.get_or_err("previous-sequence").map(Path::new)
+ }
+
+ pub fn unseen_sequence(&self) -> Result<&Path, String> {
+ self.get_or_err("unseen-sequence").map(Path::new)
+ }
+ }
+}
+
+/// Prefixed view of profile; only sees components whose name starts with the prefix.
+pub struct Prefixed<'a> {
+ prefix: &'a str,
+}
+
+impl<'a> Prefixed<'a> {
+ /// Prefixed form of [crate::Profile::get].
+ pub fn get(&self, name: &str) -> Option<&str> {
+ get(&format!("{}{}", self.prefix, name))
+ }
+
+ /// Prefixed form of [crate::Profile::get_or_err].
+ pub fn get_or_err(&self, name: &str) -> Result<&str, String> {
+ get_or_err(&format!("{}{}", self.prefix, name))
+ }
+
+ pub fn iter(&self) -> PrefixedIterator<'a> {
+ PrefixedIterator {
+ prefix: self.prefix,
+ np: unsafe { sbr::m_defs },
+ }
+ }
+}
+
+pub struct PrefixedIterator<'a> {
+ prefix: &'a str,
+ np: *mut sbr::node,
+}
+
+impl<'a> Iterator for PrefixedIterator<'a> {
+ type Item = (&'a str, &'a std::ffi::CStr);
+ fn next(&mut self) -> Option<Self::Item> {
+ while !self.np.is_null() {
+ let node = self.np;
+ self.np = unsafe { (*node).n_next };
+ let name_p: *mut c_char = unsafe { (*node).n_name };
+ assert!(!name_p.is_null(), "illegal NULL component name");
+ let name_cstr = unsafe { std::ffi::CStr::from_ptr(name_p) };
+ let name: &str = match name_cstr.to_str() {
+ Err(e) => panic!(
+ "{} decoding component name {}",
+ e,
+ name_cstr.to_string_lossy()
+ ),
+ Ok(value) => value,
+ };
+ if name.len() < self.prefix.len() {
+ continue;
+ }
+ if !name[..self.prefix.len()].eq_ignore_ascii_case(self.prefix) {
+ continue;
+ }
+
+ let value_p: *mut c_char = unsafe { (*node).n_field };
+ assert!(!value_p.is_null(), "illegal NULL component value");
+ let value_cstr = unsafe { std::ffi::CStr::from_ptr(value_p) };
+ return Some((&name[self.prefix.len()..], value_cstr));
+ }
+ None
+ }
+}
+
+fn get(name: &str) -> Option<&'static str> {
+ let mut name_bytes: Vec<u8> = match std::ffi::CString::new(name) {
+ Err(_) => return None,
+ Ok(value) => value.into_bytes_with_nul(),
+ };
+ let name_c = name_bytes.as_mut_ptr() as *mut c_char;
+ let result: *mut c_char = unsafe { sbr::context_find(name_c) };
+ if result.is_null() {
+ return None;
+ }
+ let result_c = unsafe { std::ffi::CStr::from_ptr(result) };
+ match result_c.to_str() {
+ Err(e) => panic!("{} decoding {}", e, result_c.to_string_lossy()),
+ Ok(value) => Some(value),
+ }
+}
+
+fn get_or_err(name: &str) -> Result<&'static str, String> {
+ get(name).ok_or_else(|| format!("required component {} absent from profile", name))
+}
--- /dev/null
+use crate::bindgen;
+
+pub use bindgen::context_find;
+pub use bindgen::m_defs;
+pub use bindgen::nmh_init;
+pub use bindgen::node;
+
+pub use bindgen::m_getfld2;
+pub use bindgen::m_getfld_state_destroy;
+pub use bindgen::m_getfld_state_init;
+pub use bindgen::m_getfld_state_t;
+pub use bindgen::FMTERR;
+pub use bindgen::LENERR;
+pub use bindgen::NAMESZ;
+
+pub use bindgen::netsec_context;
+pub use bindgen::netsec_err;
+pub use bindgen::netsec_flush;
+pub use bindgen::netsec_get_sasl_mechanism;
+pub use bindgen::netsec_get_sasl_ssf;
+pub use bindgen::netsec_init;
+pub use bindgen::netsec_negotiate_sasl;
+pub use bindgen::netsec_negotiate_tls;
+pub use bindgen::netsec_printf;
+pub use bindgen::netsec_read;
+pub use bindgen::netsec_readline;
+pub use bindgen::netsec_sasl_callback;
+pub use bindgen::netsec_set_fd;
+pub use bindgen::netsec_set_hostname;
+pub use bindgen::netsec_set_oauth_service;
+pub use bindgen::netsec_set_sasl_params;
+pub use bindgen::netsec_set_snoop;
+pub use bindgen::netsec_set_snoop_callback;
+pub use bindgen::netsec_set_timeout;
+pub use bindgen::netsec_set_tls;
+pub use bindgen::netsec_set_userid;
+pub use bindgen::netsec_shutdown;
+pub use bindgen::netsec_snoop_callback;
+pub use bindgen::netsec_write;
+
+pub use bindgen::netsec_b64_snoop_decoder;
+
+pub use bindgen::sasl_message_type;
+pub use bindgen::sasl_message_type_NETSEC_SASL_CANCEL;
+pub use bindgen::sasl_message_type_NETSEC_SASL_FINISH;
+pub use bindgen::sasl_message_type_NETSEC_SASL_READ;
+pub use bindgen::sasl_message_type_NETSEC_SASL_START;
+pub use bindgen::sasl_message_type_NETSEC_SASL_WRITE;
+
+pub use bindgen::decodeBase64;
+pub use bindgen::writeBase64raw;
+
+pub use bindgen::client;
+
+// TODO take from libc crate? But those are weird and require conversions?!
+pub use bindgen::fclose;
+pub use bindgen::fdopen;
+pub use bindgen::free;
+pub use bindgen::size_t;
+pub use bindgen::ssize_t;
+pub use bindgen::FILE;
--- /dev/null
+//! [String]-like types (and when to use them)
+
+//! - Use [String] and [str] in pure Rust
+//! - Use [std::ffi::CStr] to accept a reference to a null-terminated string owned in C.
+//! - Use [std::ffi::CString] to pass a reference to a [str] to C, but note that this makes a copy. If this
+//! should become a problem, [NString] is ready to support this case.
+//! - Use [NString] to pass a mutable reference to a buffer into which C may write a null-terminated string.
+//! - Use [NStringWithFree] to accept ownership of a null-terminated string from C, to be freed with sbr::free
+
+use crate::Error;
+
+// TODO implement more String-like behaviors as needed, for individual types and for OrString.
+// Consider putting those behaviors into a StringLike trait.
+
+// TODO could easily split into ASCIIString and UTF8String if needed.
+/// Like String but always with a final null byte for no-copy use with C-style functions, and always containing only ASCII.
+pub struct NString {
+ // INVARIANT: data.len() > len
+ // While this starts out all zero, it is possible for bytes between len and data.len() to be non-zero, for example:
+ // sprintf(nstring.mut_ptr(), b"this many bytes\0");
+ // sprintf(nstring.mut_ptr(), b"fewer bytes\0");
+ // At this point, data is b"fewer bytes\0tes\0" and nstring.as_str() is "fewer bytes".
+ data: Vec<u8>,
+ len: usize,
+}
+
+impl NString {
+ pub fn new() -> Self {
+ Self::with_capacity(0)
+ }
+
+ pub fn with_capacity(capacity: usize) -> Self {
+ Self {
+ data: vec![0; capacity + 1],
+ len: 0,
+ }
+ }
+
+ pub fn push_ascii_str(&mut self, s: &str) -> Result<(), Error> {
+ check_ascii(s.as_bytes())?;
+ let new_len = self.len + s.len();
+ // Resize, but only up.
+ self.data
+ .resize(std::cmp::max(self.data.len(), new_len + 1), 0);
+ self.data[self.len..new_len].clone_from_slice(s.as_bytes());
+ self.len = new_len;
+ Ok(())
+ }
+
+ #[inline]
+ pub fn as_str(&self) -> &str {
+ unsafe { std::str::from_utf8_unchecked(&self.data[..self.len]) }
+ }
+
+ /// Convert to [UnsafeNString] for ues with C.
+ pub fn into_unsafe(self) -> UnsafeNString {
+ UnsafeNString(self)
+ }
+}
+
+impl Default for NString {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl std::fmt::Debug for NString {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.as_str().fmt(f)
+ }
+}
+
+impl std::ops::Deref for NString {
+ type Target = str;
+ #[inline]
+ fn deref(&self) -> &str {
+ self.as_str()
+ }
+}
+
+impl std::ops::DerefMut for NString {
+ #[inline]
+ fn deref_mut(&mut self) -> &mut str {
+ unsafe { std::str::from_utf8_unchecked_mut(&mut self.data[..self.len]) }
+ }
+}
+
+impl std::fmt::Display for NString {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ self.as_str().fmt(f)
+ }
+}
+
+/// Form of [NString] for use with C.
+pub struct UnsafeNString(NString);
+
+impl UnsafeNString {
+ /// Return the mutable pointer to unsigned char.
+ pub fn mut_ptr(&mut self) -> *mut std::os::raw::c_uchar {
+ self.0.data.as_mut_ptr() as *mut std::os::raw::c_uchar
+ }
+
+ /// Convert back to [NString]. Unsafe because we can't know what whether your C library stomped
+ /// all over everything. `len` is the length of the string excluding the null byte. `len`
+ /// obviously MUST be less than the capacity of the underlying [NString], otherwise you've
+ /// overrun the buffer!
+ /// # Safety
+ /// Safe only if caller avoids writing past the end of the buffer.
+ pub unsafe fn to_nstring(self, len: usize) -> Result<NString, Error> {
+ to_nstring(self, len)
+ }
+
+ /// Convert back to [NString]. Unsafe because we can't know what whether your C library stomped
+ /// all over everything. In this case, you promise to have placed a null byte after your
+ /// string, and we will find it and use its offset as the string length.
+ ///
+ /// Doing this is discouraged; you probably already knew the length and lost it, and would be
+ /// better off fixing that lossage.
+ /// # Safety
+ /// Safe only if caller includes a null byte and avoids writing past the end of the buffer.
+ pub unsafe fn to_nstring_sized_by_null_byte(self) -> Result<NString, Error> {
+ to_nstring_size_by_null_byte(self)
+ }
+}
+
+pub enum OrString {
+ N(NString),
+ S(String),
+}
+
+impl std::ops::Deref for OrString {
+ type Target = str;
+ #[inline]
+ fn deref(&self) -> &str {
+ match self {
+ Self::N(ns) => ns,
+ Self::S(s) => s,
+ }
+ }
+}
+
+impl std::fmt::Debug for OrString {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ Self::N(ns) => f.debug_tuple("NString").field(ns).finish(),
+ Self::S(s) => f.debug_tuple("String").field(s).finish(),
+ }
+ }
+}
+
+impl std::fmt::Display for OrString {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.write_str(self)
+ }
+}
+
+impl From<String> for OrString {
+ fn from(s: String) -> Self {
+ Self::S(s)
+ }
+}
+
+impl From<&str> for OrString {
+ fn from(s: &str) -> Self {
+ Self::S(s.to_owned())
+ }
+}
+
+/// See module documentation.
+/// WARNING: This frees the buffer, so only use when caller owns!
+/// Don't reuse in order to avoid memory leak; in fact, this panics if you attempt reuse.
+pub struct NStringWithFree {
+ pointer: *mut std::os::raw::c_char,
+ accessed: bool,
+}
+
+impl NStringWithFree {
+ pub fn new() -> Self {
+ Self {
+ pointer: std::ptr::null::<*mut std::os::raw::c_char>() as *mut std::os::raw::c_char,
+ accessed: false,
+ }
+ }
+
+ pub fn pointer(&mut self) -> *mut *mut std::os::raw::c_char {
+ assert!(
+ !self.accessed,
+ "reusing NStringWithFree risks leaking the buffer"
+ );
+ self.accessed = true;
+ &mut self.pointer as *mut *mut _ as *mut *mut std::os::raw::c_char
+ }
+}
+
+impl Default for NStringWithFree {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl std::convert::TryFrom<NStringWithFree> for String {
+ type Error = Error;
+ fn try_from(value: NStringWithFree) -> Result<Self, Self::Error> {
+ if value.pointer.is_null() {
+ return Err(Error::new(
+ "BUG: TryFrom<NStringWithFree> for String buffer pointer is null".to_owned(),
+ ));
+ }
+ unsafe { std::ffi::CStr::from_ptr(value.pointer) }
+ .to_str()
+ .map_err(Error::wrap)
+ .map(|v| v.to_owned())
+ }
+}
+
+impl Drop for NStringWithFree {
+ fn drop(&mut self) {
+ let void = self.pointer as *mut std::os::raw::c_void;
+ unsafe { crate::sbr::free(void) }
+ }
+}
+
+fn check_ascii(bytes: &[u8]) -> Result<(), Error> {
+ let as_str: &str = unsafe { std::str::from_utf8_unchecked(bytes) };
+ if !as_str.is_ascii() {
+ Err(Error::new(format!("not ASCII: {}", as_str)))
+ } else {
+ Ok(())
+ }
+}
+
+fn to_nstring(mut rs: UnsafeNString, len: usize) -> Result<NString, Error> {
+ assert!(
+ len < rs.0.data.len(),
+ "{} >= {}; buffer overrun likely",
+ len,
+ rs.0.data.len()
+ );
+ rs.0.len = len;
+ check_ascii(&rs.0.data[..rs.0.len])?;
+ Ok(rs.0)
+}
+
+fn to_nstring_size_by_null_byte(rs: UnsafeNString) -> Result<NString, Error> {
+ let len: usize =
+ rs.0.data
+ .iter()
+ .position(|c| *c == 0)
+ .expect("UnsafeNString lacks null byte; buffer overrun likely");
+ to_nstring(rs, len)
+}
+
+#[cfg(test)]
+mod tests;
--- /dev/null
+use super::*;
+
+mod nstring {
+ use super::*;
+
+ /// Test [NString] starting out holding N bytes, and then being shortened to fewer bytes.
+ #[test]
+ fn long_then_short() {
+ let mut ns = NString::new();
+ ns.push_ascii_str("this many bytes").unwrap();
+ let mut raw = ns.into_unsafe();
+ sprintf(raw.mut_ptr(), b"fewer bytes\0");
+ ns = unsafe { raw.to_nstring_sized_by_null_byte() }.unwrap();
+ // The reachable part of the NString is short.
+ assert_eq!("fewer bytes", ns.as_str());
+ // The stale bytes remain untouched but unaccessed in the underlying buffer.
+ assert_eq!(b"fewer bytes\0tes\0", &ns.data[..]);
+ }
+}
+
+fn sprintf(format: *mut std::os::raw::c_uchar, s: &[u8]) {
+ unsafe {
+ libc::sprintf(
+ format as *mut std::os::raw::c_char,
+ s.as_ptr() as *const std::os::raw::c_char,
+ )
+ };
+}
--- /dev/null
+use nmh::base64::encode_raw;
+
+const DECODED: &'static [u8] = b"some bytes\nand lines\r\nbut mixed up";
+const DECODED_EMBEDDED_NULL: &'static [u8] =
+ b"some bytes\nand lines\r\nbut mixed up\0was that a null";
+static ENCODED: &str = "c29tZSBieXRlcwphbmQgbGluZXMNCmJ1dCBtaXhlZCB1cA==";
+static ENCODED_EMBEDDED_NULL: &str =
+ "c29tZSBieXRlcwphbmQgbGluZXMNCmJ1dCBtaXhlZCB1cAB3YXMgdGhhdCBhIG51bGw=";
+
+#[test]
+fn round_trip_encode_raw() {
+ assert_eq!(encode_raw(DECODED).as_str(), ENCODED);
+}
+
+#[test]
+fn round_trip_embedded_null() {
+ assert_eq!(
+ encode_raw(DECODED_EMBEDDED_NULL).as_str(),
+ ENCODED_EMBEDDED_NULL
+ );
+}
--- /dev/null
+use std::error::Error;
+
+use nmh::client::*;
+
+#[test]
+fn server_null_byte() {
+ let e = Connector::new()
+ .service("imap")
+ .unwrap()
+ .server("\0")
+ .expect_err("need error");
+ let source = e.source().expect(&format!("need source in {:?}", e));
+ assert_eq!(
+ 0,
+ (*source)
+ .downcast_ref::<std::ffi::NulError>()
+ .expect(&format!("need NulError: {:?}", source))
+ .nul_position()
+ )
+}
+
+#[test]
+fn service_null_byte() {
+ let e = Connector::new()
+ .server("localhost")
+ .unwrap()
+ .service("\0")
+ .expect_err("need error");
+ let source = e.source().expect(&format!("need source in {:?}", e));
+ assert_eq!(
+ 0,
+ (*source)
+ .downcast_ref::<std::ffi::NulError>()
+ .expect(&format!("need NulError: {:?}", source))
+ .nul_position()
+ )
+}
+
+#[test]
+fn empty_strings() {
+ assert_eq!(
+ "Internal error: NULL server name passed to client()",
+ Connector::new()
+ .server("")
+ .unwrap()
+ .service("")
+ .unwrap()
+ .connect()
+ .expect_err("need error")
+ .to_string()
+ )
+}
--- /dev/null
+#[test]
+fn option_no_overhead() {
+ assert_eq!(
+ std::mem::size_of::<i32>(),
+ std::mem::size_of::<Option<nmh::message::Number>>()
+ );
+}
+
+#[test]
+fn max() {
+ nmh::message::Number::from(std::num::NonZeroI32::new(i32::MAX).unwrap());
+}
+
+#[test]
+#[should_panic(expected = "attempt to add with overflow")]
+fn increment_max() {
+ let mut n = nmh::message::Number::from(std::num::NonZeroI32::new(i32::MAX).unwrap());
+ n.increment();
+}
+
+#[test]
+fn clone() {
+ let n = nmh::message::Number::one();
+ assert_eq!(n, n.clone());
+}
+
+#[test]
+fn display() {
+ assert_eq!("1", format!("{}", nmh::message::Number::one()));
+}
+
+#[test]
+fn debug() {
+ assert_eq!("Number(1)", format!("{:?}", nmh::message::Number::one()));
+}
+
+#[test]
+fn eq() {
+ let a = nmh::message::Number::one();
+ let b = nmh::message::Number::from(std::num::NonZeroI32::new(2).unwrap());
+ assert!(a == a);
+ assert!(!(a == b));
+ assert!(!(a != a));
+ assert!(a != b);
+}
+
+mod set;
--- /dev/null
+use nmh::message::Set;
+
+mod display {
+ use super::*;
+
+ #[test]
+ fn one() {
+ let mut set = Set::new();
+ set.insert(1);
+ assert_eq!("1", &format!("{}", set))
+ }
+
+ #[test]
+ fn some() {
+ let mut set = Set::new();
+ set.insert(1);
+ set.insert(2);
+ set.insert(3);
+ set.insert(10);
+ set.insert(15);
+ set.insert(16);
+ set.insert(17);
+ assert_eq!("1-3 10 15-17", &format!("{}", set))
+ }
+
+ #[test]
+ fn all() {
+ assert_eq!("1-2147483647", &format!("{}", Set::all()))
+ }
+}
+
+mod all {
+ use super::*;
+
+ #[test]
+ fn append_some_to_all() {
+ let mut all = Set::all();
+ let mut one = Set::new();
+ one.insert(1);
+ all.append(&mut one);
+ assert_eq!("1-2147483647", &format!("{}", all))
+ }
+
+ #[test]
+ fn append_all_to_some() {
+ let mut all = Set::all();
+ let mut one = Set::new();
+ one.insert(1);
+ one.append(&mut all);
+ assert_eq!("1-2147483647", &format!("{}", one))
+ }
+
+ #[test]
+ fn insert_to_all() {
+ let mut all = Set::all();
+ all.insert(1);
+ assert_eq!("1-2147483647", &format!("{}", all))
+ }
+
+ #[test]
+ fn clone_without() {
+ let mut some = Set::new();
+ some.insert(1);
+ assert!(some.clone_without(&Set::all()).is_empty())
+ }
+
+ #[test]
+ fn is_empty() {
+ assert!(!Set::all().is_empty())
+ }
+}
--- /dev/null
+use std::error::Error;
+use std::ffi::{CString, NulError};
+
+use nmh::Netsec;
+
+mod builder {
+ use super::*;
+
+ #[test]
+ fn timeout_out_of_range() {
+ let e = Netsec::builder()
+ .timeout(std::time::Duration::from_secs(u64::MAX))
+ .expect_err("need error");
+ let source = e.source().expect(&format!("need source in {:?}", e));
+ assert!(
+ (*source)
+ .downcast_ref::<std::num::TryFromIntError>()
+ .is_some(),
+ "need TryFromIntError: {:?}",
+ e
+ )
+ }
+
+ #[test]
+ fn userid_with_nulls() {
+ let e = Netsec::builder()
+ .host(&CString::new("a hostname").unwrap())
+ .sasl_userid("oh\0null")
+ .expect_err("need error");
+ let source = e.source().expect(&format!("need source in {:?}", e));
+ assert_eq!(
+ 2,
+ (*source)
+ .downcast_ref::<NulError>()
+ .expect(&format!("need NulError: {:?}", source))
+ .nul_position()
+ )
+ }
+
+ #[test]
+ fn sasl_service_with_nulls() {
+ let e = Netsec::builder()
+ .host(&CString::new("a hostname").unwrap())
+ .sasl_service("oh\0null")
+ .expect_err("need error");
+ let source = e.source().expect(&format!("need source in {:?}", e));
+ assert_eq!(
+ 2,
+ (*source)
+ .downcast_ref::<NulError>()
+ .expect(&format!("need NulError: {:?}", source))
+ .nul_position()
+ )
+ }
+
+ #[test]
+ fn user_preferred_sasl_mechanism_with_nulls() {
+ let e = Netsec::builder()
+ .host(&CString::new("a hostname").unwrap())
+ .sasl_prefer_mechanism("oh\0null")
+ .expect_err("need error");
+ let source = e.source().expect(&format!("need source in {:?}", e));
+ assert_eq!(
+ 2,
+ (*source)
+ .downcast_ref::<NulError>()
+ .expect(&format!("need NulError: {:?}", source))
+ .nul_position()
+ )
+ }
+}
--- /dev/null
+use std::convert::TryFrom as _;
+
+use nmh::str::*;
+
+mod nstring {
+ use super::*;
+
+ #[test]
+ fn empty() {
+ assert!(NString::with_capacity(1).as_str().is_empty());
+ }
+
+ #[test]
+ fn capacity_zero_is_empty() {
+ assert!(NString::with_capacity(0).as_str().is_empty());
+ }
+
+ #[test]
+ fn push_str_non_ascii() {
+ let mut ns = NString::new();
+ assert_eq!(
+ "not ASCII: ¯\\_(ツ)_/¯",
+ ns.push_ascii_str(SHRUG).unwrap_err().to_string()
+ );
+ }
+
+ #[test]
+ fn sprintf_non_ascii() {
+ let mut shrug = SHRUG.as_bytes().to_vec();
+ shrug.push(0);
+ let ns = NString::with_capacity(32);
+ let mut raw = ns.into_unsafe();
+ sprintf(raw.mut_ptr(), &shrug);
+ assert_eq!(
+ "not ASCII: ¯\\_(ツ)_/¯",
+ unsafe { raw.to_nstring_sized_by_null_byte() }
+ .unwrap_err()
+ .to_string()
+ );
+ }
+
+ #[test]
+ #[should_panic(expected = "UnsafeNString lacks null byte; buffer overrun likely")]
+ fn missing_null_byte() {
+ let ns = NString::with_capacity(0);
+ let mut raw = ns.into_unsafe();
+ unsafe {
+ *(raw.mut_ptr()) = 1;
+ raw.to_nstring_sized_by_null_byte().unwrap();
+ }
+ }
+
+ #[test]
+ #[should_panic(expected = "10 >= 1; buffer overrun likely")]
+ fn too_many_bytes() {
+ let ns = NString::with_capacity(0);
+ let raw = ns.into_unsafe();
+ unsafe { raw.to_nstring(10).unwrap() };
+ }
+
+ #[test]
+ fn deref_mut() {
+ let mut ns = NString::new();
+ ns.push_ascii_str("foo").unwrap();
+ let mut_str: &mut str = &mut ns;
+ mut_str.make_ascii_uppercase();
+ assert_eq!("FOO", ns.as_str());
+ }
+
+ mod with_free {
+ use super::*;
+
+ #[test]
+ fn to_string_with_null() {
+ assert_eq!(
+ "BUG: TryFrom<NStringWithFree> for String buffer pointer is null",
+ String::try_from(NStringWithFree::new())
+ .unwrap_err()
+ .to_string(),
+ );
+ }
+ }
+}
+
+const SHRUG: &str = "¯\\_(ツ)_/¯";
+
+fn sprintf(format: *mut std::os::raw::c_uchar, s: &[u8]) {
+ unsafe {
+ libc::sprintf(
+ format as *mut std::os::raw::c_char,
+ s.as_ptr() as *const std::os::raw::c_char,
+ )
+ };
+}
--- /dev/null
+mod base64;
+mod client;
+mod message;
+mod netsec;
+mod str;
{
unsigned int i;
int j, k, is_current;
- char *cp, **ap;
+ char *cp;
/*
* Check if this is "cur" sequence,
/*
* Split up the different message ranges at whitespace
*/
- for (ap = brkstring (field, " ", "\n"); *ap; ap++) {
+ for (char *const *ap = brkstring (field, " ", "\n"); *ap; ap++) {
if ((cp = strchr(*ap, '-')))
*cp++ = '\0';
if ((j = m_atoi (*ap)) > 0) {
*/
void
-seq_setprev (struct msgs *mp)
+seq_setprev (struct msgs *const mp)
{
- char **ap, *cp, *dp;
-
/*
* Get the list of sequences for Previous-Sequence
* and split them.
*/
- if (!(cp = context_find(psequence)))
+ const char *const maybe_previous_sequences = context_find(psequence);
+ if (maybe_previous_sequences == NULL)
return;
- dp = mh_xstrdup(cp);
- if (!(ap = brkstring(dp, " ", "\n")) || !*ap) {
- free(dp);
+ char *const previous_sequences = mh_xstrdup(maybe_previous_sequences);
+ char *const *broken = brkstring(previous_sequences, " ", "\n");
+ if (broken == NULL || *broken == NULL) {
+ free(previous_sequences);
return;
}
/* Now add all SELECTED messages to each sequence */
- for (; *ap; ap++)
- seq_addsel (mp, *ap, -1, 1);
+ for (; *broken; broken++)
+ seq_addsel (mp, *broken, -1, 1);
- free (dp);
+ free (previous_sequences);
}
*/
void
-seq_setunseen (struct msgs *mp, int seen)
+seq_setunseen (struct msgs *const mp, const int seen)
{
- int msgnum;
- char **ap, *cp, *dp;
-
/*
* Get the list of sequences for Unseen-Sequence
* and split them.
*/
- if (!(cp = context_find (usequence)))
+ const char*const maybe_unseen_sequences = context_find (usequence);
+ if (maybe_unseen_sequences == NULL)
return;
- dp = mh_xstrdup(cp);
- if (!(ap = brkstring (dp, " ", "\n")) || !*ap) {
- free (dp);
+ char *const unseen_sequences = mh_xstrdup(maybe_unseen_sequences);
+ char *const *broken = brkstring (unseen_sequences, " ", "\n");
+ if (broken == NULL || *broken == NULL) {
+ free (unseen_sequences);
return;
}
* Now add/delete each message which has the SELECT_UNSEEN
* bit set to/from each of these sequences.
*/
- for (; *ap; ap++) {
+ for (; *broken; broken++) {
if (seen) {
/* make sure sequence exists first */
- if (seq_getnum(mp, *ap) != -1)
- for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++)
+ if (seq_getnum(mp, *broken) != -1)
+ for (int msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++)
if (is_unseen (mp, msgnum))
- seq_delmsg (mp, *ap, msgnum);
+ seq_delmsg (mp, *broken, msgnum);
} else {
- for (msgnum = mp->lowmsg; msgnum <= mp->hghmsg; msgnum++)
+ for (int msgnum = mp->lowmsg; msgnum <= mp->hghmsg; msgnum++)
if (is_unseen (mp, msgnum))
- seq_addmsg (mp, *ap, msgnum, -1, 0);
+ seq_addmsg (mp, *broken, msgnum, -1, 0);
}
}
- free (dp);
+ free (unseen_sequences);
}
int
-showfile (char **arg, char *file)
+showfile (char *const *arg, char *const file)
{
pid_t pid;
int vecp;
* COPYRIGHT file in the root directory of the nmh distribution for
* complete copyright information. */
-int showfile(char **, char *);
+int showfile(char *const *, char *);
return vec->strs[i];
}
+char *
+svector_read_at (const struct svector *const vec, size_t i)
+{
+ if (i >= vec->maxsize)
+ return NULL;
+ return vec->strs[i];
+}
+
/*
* Return address of first element that stringwise matches s.
* The caller can replace the contents of the return address.
*/
char **
-svector_find (svector_t vec, const char *s)
+svector_find (const struct svector *const vec, const char *s)
{
size_t i;
char **str = vec->strs;
}
char **
-svector_strs (svector_t vec)
+svector_strs (const struct svector *const vec)
{
return vec->strs;
}
size_t
-svector_size (svector_t vec)
+svector_size (const struct svector *const vec)
{
return vec->size;
}
void svector_free(svector_t) NONNULL(1);
char *svector_push_back(svector_t, char *) NONNULL(1);
char *svector_at(svector_t, size_t) NONNULL(1);
-char **svector_find(svector_t, const char *) NONNULL(1) PURE;
-char **svector_strs(svector_t) NONNULL(1) PURE;
-size_t svector_size(svector_t) NONNULL(1) PURE;
+char *svector_read_at(const struct svector *, size_t) NONNULL(1);
+char **svector_find(const struct svector *, const char *) NONNULL(1) PURE;
+char **svector_strs(const struct svector *) NONNULL(1) PURE;
+size_t svector_size(const struct svector *) NONNULL(1) PURE;
typedef struct ivector *ivector_t;
# Common helper routines for test shell scripts -- to be sourced by them
# @configure_input@
+exec < /dev/null
#### The following variables are set by "make check". Ensure
#### that they are set here so that individual tests can be run
HOME="$MH_TEST_DIR"
export HOME
+unset EDITOR
unset MAILDROP MHBUILD MHCONTEXT MHMTSUSERCONF MHN MHSHOW MHSTORE
unset MHLDEBUG MHWDEBUG PAGER XOAUTH SMTPUTF8
#### Set LC_ALL in individual tests as needed. Unset these so
sed 's/'"`printf '\\302\\240'`"'/ /g'
}
+#### fail increments the global variable "failed" and return non-zero.
+#### Any parameters are passed to echo.
+fail() {
+ if [ x"$1" != x ]; then
+ echo "$@"
+ fi
+ failed=`expr ${failed:-0} + 1`
+ false
+}
+
#### check() requires two arguments, each the name of a file to be
#### diff'ed.
#### If the contents are same, the second file is removed. If different,
#### POSIX diff should support -c.
diff -c "$first" "$second" || true
echo
- echo "$0: $label failed, outputs are in $first and $second."
- failed=`expr ${failed:-0} + 1`
- #### Set return status of the function.
- [ $failed -eq 0 ]
+ fail "$0: $label failed, outputs are in $first and $second."
fi
}
#### cd to $MH_TEST_DIR before trying to remove its Mail
#### subdirectory. rm on Solaris won't remove it if it's in the
#### path of the current working directory.
+ # TODO multi-profile?
test -z "$MH_TEST_NOCLEANUP" && (cd "$MH_TEST_DIR" && rm -rf "$MH_TEST_DIR"/Mail)
#### Report test name if set, which indicates failure.
#### Function to set the test name, and whatever the future brings.
start_test() {
+ [ -n "$MH_TEST_VERBOSE" ] && echo "START TEST $1"
#### run_test disables exit on non-zero status, but does increment
#### failed. Don't overwrite nmh_tests_testname if there was a
#### failure; remember the first test that failed.
[ ${failed:-0} -eq 0 ] && unset nmh_tests_testname
}
-setup_test ()
+set_mh_profile()
{
- set -e
+ profile="$1"
+ MH="${MH_TEST_DIR}/$profile/.mh_profile"
+ MHMTSCONF="${MH_INST_DIR}${nmhetcdir}/mts.conf"
+ MHTMPDIR="$MH_TEST_DIR/$profile"
+ MH_LIBEXEC_DIR="${MH_INST_DIR}${nmhlibexecdir}"
+ export MH MHMTSCONF MHTMPDIR MH_LIBEXEC_DIR
+}
- MH="${MH_TEST_DIR}/Mail/.mh_profile"
- MHMTSCONF="${MH_INST_DIR}${nmhetcdir}/mts.conf"
- MH_LIBEXEC_DIR="${MH_INST_DIR}${nmhlibexecdir}"
- export MH MHMTSCONF MH_LIBEXEC_DIR
+setup_test_without_data ()
+{
+ set -e
+ set_mh_profile "$1"
+ profiles="$profiles $profile"
#
# Only install once
:
else
(cd "${MH_OBJ_DIR}" &&
- make DESTDIR="${MH_INST_DIR}" SETGID_MAIL= install) ||
- exit 1
+ make DESTDIR="${MH_INST_DIR}" SETGID_MAIL= install)
#### Don't test with sendmail because it would really send the
#### mail. If configured to use sendmail, change to smtp instead
#### test-slocal needs to look at the original mts.conf, so save it.
mv -f "${MHMTSCONF}" "${MHMTSCONF}.old"
sed -e 's/mts: *.*/mts: smtp/' \
- -e "s%mmdfldir: *.*%mmdfldir: ${MH_TEST_DIR}/Mail%" \
+ -e "s%mmdfldir: *.*%mmdfldir: ${MH_TEST_DIR}/$1%" \
-e 's%mmdflfil: *.*%mmdflfil: maildrop%' \
"${MHMTSCONF}.old" >"${MHMTSCONF}"
fi
+ # TODO we prepend it again for every new profile...
#### On Solaris, must set PATH after the install!
PATH="${MH_INST_DIR}${bindir}:${PATH}"
export PATH
# clean old test data on exit
trap cleanup 0
- # setup test data
- mkdir -p "$MH_TEST_DIR/Mail" || exit 1
- cat > "$MH" <<EOF || exit 1
-Path: ${MH_TEST_DIR}/Mail
+ # But crashes happen; cleanup here too.
+ rm -rf "$MH_TEST_DIR/$profile"
+
+ mkdir -p "$MH_TEST_DIR/$1"
+ cat > "$MH" <<EOF
+Path: ${MH_TEST_DIR}/$1
buildmimeproc: ${MH_INST_DIR}${bindir}/mhbuild
fileproc: ${MH_INST_DIR}${bindir}/refile
libexecdir: ${MH_LIBEXEC_DIR}
#### installation.
#### posh doesn't like "${MH_INST_DIR}${nmhetcdir}"/*, so cd to
#### the directory and provide an argument without quotes to cp.
- (cd "${MH_INST_DIR}${nmhetcdir}/" && cp * "${MH_TEST_DIR}/Mail")
+ (cd "${MH_INST_DIR}${nmhetcdir}/" && cp * "${MH_TEST_DIR}/$1")
fi
+}
+
+setup_test_data ()
+{
+ set -e
folder -create +inbox > /dev/null
#### Suppress warnings about a new version.
#### mhparam version includes the nmh- prefix, so be consistent with that.
- printf 'Version: nmh-%s\n' "${MH_VERSION}" >> "${MH_TEST_DIR}"/Mail/context
+ printf 'Version: nmh-%s\n' "${MH_VERSION}" >> "${MH_TEST_DIR}/$1/context"
# create 10 basic messages
for i in 1 2 3 4 5 6 7 8 9 10;
do
- cat > $MH_TEST_DIR/Mail/inbox/$i <<EOF || exit 1
+ cat > "$MH_TEST_DIR/$1/inbox/$i" <<EOF || exit 1
From: Test$i <test$i@example.com>
To: Some User <user@example.com>
Date: Fri, 29 Sep 2006 00:00:00
EOF
done
}
+
+setup_test ()
+{
+ set -e
+
+ setup_test_without_data Mail
+ setup_test_data Mail
+}
+
+### Start fakeinetd.
+### All arguments are passed as-is to fakeinetd.
+### If successul, returns 0 and sets parameters fakeinetd_pid and fakeinetd_port.
+start_fakeinetd() {
+ fakeinetd_pid_port=$("$MH_OBJ_DIR/test/fakeinetd" "$@") || return
+ set -- $fakeinetd_pid_port
+ unset fakeinetd_pid_port
+ [ $# -eq 2 ] || return
+ fakeinetd_pid=$1
+ fakeinetd_port=$2
+}
+
+# TODO autoconf
+STUNNEL=stunnel
+
+### Start stunnel under fakeinetd.
+### start_stunnel NAME EXEC
+### See start_fakeinetd above.
+start_stunnel() {
+ cat > "$MH_TEST_DIR/$1.stunnel.cf" <<EOF
+foreground = yes
+output = ${MH_TEST_STUNNEL_OUTPUT:-/dev/null}
+syslog = no
+cert = $MH_OBJ_DIR/fake.pem
+key = $MH_OBJ_DIR/fake.pem
+exec = $2
+EOF
+ if [ -n "$MH_TEST_STUNNEL_DEBUG" ]; then
+ echo "debug = $MH_TEST_STUNNEL_DEBUG" >> "$MH_TEST_DIR/$1.stunnel.cf"
+ fi
+ cat > "$MH_TEST_DIR/$1.stunnel.sh" <<EOF
+#!/bin/sh
+exec $STUNNEL "$MH_TEST_DIR/$1.stunnel.cf" 2> ${MH_TEST_STUNNEL_OUTPUT:-/dev/null}
+EOF
+ chmod 755 "$MH_TEST_DIR/$1.stunnel.sh"
+ start_fakeinetd "$MH_TEST_DIR/$1.stunnel.sh"
+}
--- /dev/null
+#!/bin/sh
+
+if test -z "${MH_OBJ_DIR}"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "${MH_OBJ_DIR}/test/common.sh"
+
+setup_test
+
+cat >> "$MH" <<EOF
+Draft-Folder: drafts
+Local-Mailbox: norm@example.com
+EOF
+
+# Set this for the EHLO command
+echo "clientname: nosuchhost.example.com" >> ${MHMTSCONF}
+
+cat > "$MH_TEST_DIR/Mail/scan.dateless" <<EOF
+%4(msg)%<(cur)+%| %>%<(unseen)U%| %>%<{replied}-%| %>\
+%<(mymbox{from})%<{to}To:%(unquote(decode(friendly{to})))%>%>\
+%<(zero)%(unquote(decode(friendly{from})))%> \
+%(decode{subject})%<{body}<<%{body}>>%>
+EOF
+
+sbackup="`mhparam sbackup`"
+
+arith_eval 64000 + $$ % 1000
+smtp_port=$arith_val
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+################################################################################
+start_test 'comp with Draft-Folder'
+
+check_exit '-eq 0' comp -noedit -to phyl@example.com -fcc sent -subject test -nowhatnowproc < /dev/null
+
+################################################################################
+start_test 'scan +drafts shows the draft'
+
+cat > "$expected" <<EOF
+ 1+ To:phyl@example.com test
+EOF
+
+run_prog scan -form scan.dateless +drafts > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'send with Draft-Folder'
+
+pid=`"${MH_OBJ_DIR}/test/fakesmtp" /dev/null $smtp_port`
+
+if ! (echo y | check_exit '-eq 0' send -server 127.0.0.1 -port $smtp_port); then
+ failed=`expr ${failed:-0} + 1`
+fi
+kill $pid 2> /dev/null || :
+[ ${failed:-0} -eq 0 ]
+
+################################################################################
+start_test 'drafts/.mh_sequences'
+
+cat > "$expected" <<EOF
+cur: 1
+EOF
+check "`mhpath +drafts`/.mh_sequences" "$expected"
+
+################################################################################
+start_test 'scan +drafts shows no messages'
+
+cat > "$expected" <<EOF
+scan: no messages in drafts
+EOF
+
+run_prog scan -form scan.dateless +drafts > "$actual" 2>&1 || :
+check "$actual" "$expected"
+
+################################################################################
+start_test 'scan +sent shows the sent message'
+
+cat > "$expected" <<EOF
+ 1 To:phyl@example.com test
+EOF
+
+run_prog scan -form scan.dateless +sent > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'drafts/,1.orig is the original draft'
+
+cat > "$expected" <<EOF
+From: norm@example.com
+To: phyl@example.com
+cc:
+Fcc: sent
+Subject: test
+--------
+EOF
+check "`mhpath +drafts`/,1.orig" "$expected"
+
+################################################################################
+start_test 'drafts/,1 is the post-mhbuild draft'
+
+cat > "$expected" <<EOF
+From: norm@example.com
+To: phyl@example.com
+cc:
+Fcc: sent
+Subject: test
+MIME-Version: 1.0
+Content-Type: text/plain
+EOF
+check "`mhpath +drafts`/,1" "$expected"
+
+################################################################################
+# Now test with -draftfolder.
+# TODO Why are these different?!
+################################################################################
+
+################################################################################
+start_test 'comp with Draft-Folder now for send -draftfolder'
+
+check_exit '-eq 0' comp -noedit -to phyl@example.com -fcc sent -subject test2 -nowhatnowproc < /dev/null
+
+################################################################################
+start_test 'scan +drafts shows the draft now for send -draftfolder'
+
+cat > "$expected" <<EOF
+ 1+ To:phyl@example.com test2
+EOF
+
+run_prog scan -form scan.dateless +drafts > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'send -draftfolder'
+
+pid=`"${MH_OBJ_DIR}/test/fakesmtp" /dev/null $smtp_port`
+
+if ! check_exit '-eq 0' send -draftfolder drafts -server 127.0.0.1 -port $smtp_port < /dev/null; then
+ failed=`expr ${failed:-0} + 1`
+fi
+kill $pid 2> /dev/null || :
+[ ${failed:-0} -eq 0 ]
+
+################################################################################
+start_test 'drafts/.mh_sequences'
+
+cat > "$expected" <<EOF
+cur: 1
+EOF
+check "`mhpath +drafts`/.mh_sequences" "$expected"
+
+################################################################################
+start_test 'scan +drafts shows no messages now for send -draftfolder'
+
+cat > "$expected" <<EOF
+scan: no messages in drafts
+EOF
+
+run_prog scan -form scan.dateless +drafts > "$actual" 2>&1 || :
+check "$actual" "$expected"
+
+################################################################################
+start_test 'scan +sent shows the sent message now for send -draftfolder'
+
+cat > "$expected" <<EOF
+ 1 To:phyl@example.com test
+ 2 To:phyl@example.com test2
+EOF
+
+run_prog scan -form scan.dateless +sent > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test "drafts/${sbackup}1.orig is the original draft now for send -draftfolder"
+
+cat > "$expected" <<EOF
+From: norm@example.com
+To: phyl@example.com
+cc:
+Fcc: sent
+Subject: test2
+--------
+EOF
+check "`mhpath +drafts`/${sbackup}1.orig" "$expected"
+
+################################################################################
+start_test "drafts/${sbackup}1 is the post-mhbuild draft now for send -draftfolder"
+
+cat > "$expected" <<EOF
+From: norm@example.com
+To: phyl@example.com
+cc:
+Fcc: sent
+Subject: test2
+MIME-Version: 1.0
+Content-Type: text/plain
+EOF
+check "`mhpath +drafts`/${sbackup}1" "$expected"
+
+finish_test
+
+exit ${failed:-0}
--- /dev/null
+/* fakeinetd - a simple inetd-alike (more like tcpserver-alike) for tests
+ *
+ * This code is Copyright (c) 2021, by the authors of nmh. See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ *
+ * fakeinetd listens for TCP connections to a system-assigned port on 127.0.0.1,
+ * handing accepted connections to a child process for processing. The first
+ * argument to fakeinetd is the name of the handler program to run and all other
+ * arguments are passed to it. Its standard input and output will be the
+ * accepted socket with standard error inherited.
+ *
+ * If fakeinetd is able to begin accepting connections, it forks itself into
+ * the background, prints a single line to its standard output, then closes
+ * its standard output.
+ *
+ * That single line consists of the PID, a space, and the listening port.
+ */
+
+// TODO duplicated from server.c
+// Why not take the listening code out of fake*.c and run under this?
+
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+
+#include <errno.h>
+#include <signal.h>
+#include <stdio.h>
+#include <unistd.h>
+
+int
+main(int argc, char *argv[])
+{
+ if (close(0) < 0) {
+ perror("close(0)");
+ return 1;
+ }
+
+ // We're not interested in children; ignore this to avoid zombies.
+ signal(SIGCHLD, SIG_IGN);
+
+ int const listening = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
+ if (listening < 0) {
+ perror("socket");
+ return 1;
+ }
+
+ {
+ struct sockaddr_in sin = { 0 };
+ int status = inet_pton(AF_INET, "127.0.0.1", &(sin.sin_addr));
+ switch (status) {
+ case 1:
+ break;
+ case -1:
+ perror("inet_pton(127.0.0.1)");
+ return 1;
+ case 0:
+ fputs("inet_pton rejected hard-coded 127.0.0.1?!\n", stderr);
+ return 1;
+ default:
+ fprintf(stderr, "inet_pton(127.0.0.1) nonstandard return %d\n", status);
+ return 1;
+ }
+ sin.sin_family = AF_INET;
+ if (bind(listening, (struct sockaddr *)&sin, sizeof sin) < 0) {
+ perror("bind");
+ return 1;
+ }
+ }
+
+ if (listen(listening, SOMAXCONN) < 0) {
+ perror("listen");
+ return 1;
+ }
+
+ setsid();
+ switch (fork()) {
+ case -1:
+ perror("fork");
+ return 1;
+ case 0:
+ break;
+ default:
+ return 0;
+ }
+
+ {
+ struct sockaddr_in sin = { 0 };
+ socklen_t slen = sizeof sin;
+ if (getsockname(listening, (struct sockaddr *)&sin, &slen) < 0) {
+ perror("getsockname");
+ return 1;
+ }
+ if (printf("%llu %llu\n", (unsigned long long)getpid(), (unsigned long long)ntohs(sin.sin_port)) < 0) {
+ perror("printf");
+ return 1;
+ }
+ if (fclose(stdout) != 0) {
+ perror("fclose(1)");
+ return 1;
+ }
+ }
+
+ argv++;
+ for (;;) {
+ int const connected = accept(listening, NULL, NULL);
+ if (connected < 0) {
+ if (errno == EINTR)
+ continue;
+ perror("accept");
+ return 1;
+ }
+ switch (fork()) {
+ case -1:
+ perror("fork");
+ return 1;
+ case 0:
+ if (dup2(connected, 0) < 0 || dup2(connected, 1) < 0) {
+ perror("dup2");
+ return 1;
+ }
+ if (execvp(argv[0], argv) < 0) {
+ perror("execvp");
+ return 1;
+ }
+ default:
+ if (close(connected) < 0) {
+ perror("close");
+ return 1;
+ }
+ }
+ }
+}
--- /dev/null
+#!/bin/sh
+
+# Test astring handling.
+# For now only in LIST response.
+# Gmail encodes all mailbox names in quotes and embedded quotes like "foo \" bar"
+# Panda encodes them using literals.
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+# TODO no need for xoauth in this test
+. "${srcdir}/test/oauth/common.sh"
+
+# common.sh sets this to something crazy; did i do that?!
+testname=${0#$srcdir/}
+
+# coverage notes
+# (
+# set -ex
+# . ~/.cargo/env
+# covdir="$builddir/test/covdata/${MH_COVERAGE_PROFILE:-default}/test/cargo"
+# install -d "$covdir"
+# export LLVM_PROFILE_FILE="$covdir/%m-%p.profraw"
+# export CARGO_TARGET_DIR=$builddir/target
+# export RUSTC_BOOTSTRAP=1
+# export RUSTFLAGS="-Zinstrument-coverage"
+# cargo t -- --test-threads=1
+# make cargo
+# MH_IMAP_TEST_SERVER=panda make check 'TESTS=test/imap/test-astring'
+# )
+#grcov .o/clang10 -s . --binary-path .o/clang10/target/debug/ -t html --branch --ignore-not-existing -o /coverage/default > covlog 2>&1
+
+setup_test_without_data Mail
+
+setup_service
+fake_creds <<EOF
+access-testuser: test-access
+refresh-testuser: test-refresh
+expire-testuser: 2000000000
+EOF
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ rm -f "$imapd"
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+imapd="$MH_TEST_DIR/$$.imapd"
+start_fakeinetd "$imapd"
+imapd_pid=$fakeinetd_pid
+
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: SASL
+imap-user: testuser
+imap-saslmech: xoauth2
+imap-authservice: test
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/Mail/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+# Test astring and nstring handling with some faked up but legal responses.
+# I don't know whether any implementations return nil or quoted in these FETCH
+# respones, but it is legal, so it's a fair test.
+# TODO removed nstring testing for now
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* OK [CAPABILITY IMAP4REV1 SASL-IR AUTH=PLAIN AUTH=XOAUTH2] greeting\r
+1 OK [CAPABILITY IMAP4REV1 UIDPLUS] authenticated\r
+* LIST () "/" "embedded quote \" gmail style"\r
+* LIST () "/" {28}\r
+embedded quote " panda style\r
+1 OK LIST completed\r
+* 0 EXISTS\r
+* OK [UIDVALIDITY 1603585238] UID validity status\r
+* OK [UIDNEXT 1] Predicted next UID\r
+1 OK [READ-WRITE] SELECT completed\r
+1 OK ESEARCH\r
+* 0 EXISTS\r
+* OK [UIDVALIDITY 1603585238] UID validity status\r
+* OK [UIDNEXT 1] Predicted next UID\r
+* 7 FETCH ()\r
+* 7 FETCH (UID 1 FLAGS (\Seen))\r
+1 OK [READ-WRITE] SELECT completed\r
+1 OK ESEARCH\r
+EOF
+while read line; do :; done
+EOSCRIPT
+chmod 755 "$imapd"
+
+check_exit '-eq 0' inc +imap
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+# TODO rename this file test-bad-list
+# TODO figure out how to get a server to send a BAD response so we have an accurate fake
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+# common.sh sets this to something crazy; did i do that?!
+testname=${0#$srcdir/}
+
+setup_test_without_data Mail
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ rm -f "$imapd"
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+imapd="$MH_TEST_DIR/$$.imapd"
+start_fakeinetd "$imapd"
+imapd_pid=$fakeinetd_pid
+
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: PREAUTH
+imap-mailbox-root: test/
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+start_test 'unrecoverable error: BAD LIST'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* PREAUTH [CAPABILITY IMAP4REV1 UIDPLUS] authenticated\r
+* LIST (\NoInferiors \UnMarked) "/" test/fold\r
+1 BAD fake fake fakey fake\r
+EOF
+while read line; do :; done
+EOSCRIPT
+chmod 755 "$imapd"
+
+cat > "$expected" <<EOF
+inc: failed to inc folder imap: BAD response from LIST: Unknown fake fake fakey fake
+EOF
+inc +imap > "$actual" 2>&1 || status=$?
+check "$actual" "$expected"
+(exit $status) && fail 'inc exited 0 on error'
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+# TODO FIX uh oh this and the inc missing msg cases passed by accident after inc uses ESEARCH
+# and now switching inc to leave mailbox open has broken it completely!
+
+# TODO figure out how to get a server to send a BAD response so we have an accurate fake
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+# common.sh sets this to something crazy; did i do that?!
+testname=${0#$srcdir/}
+
+setup_test_without_data Mail
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ rm -f "$imapd"
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+imapd="$MH_TEST_DIR/$$.imapd"
+start_fakeinetd "$imapd"
+imapd_pid=$fakeinetd_pid
+
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: PREAUTH
+imap-mailbox-root: test/
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+start_test 'unrecoverable error: BAD FETCH'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* PREAUTH [CAPABILITY IMAP4REV1 UIDPLUS] authenticated\r
+* LIST (\NoInferiors \UnMarked) "/" test/fold\r
+1 OK LIST completed\r
+* 1 EXISTS\r
+* OK [UIDVALIDITY 1603585238] UID validity status\r
+* OK [UIDNEXT 12] Predicted next UID\r
+1 OK [READ-WRITE] SELECT completed\r
+* SEARCH 11\r
+1 OK UID SEARCH completed\r
+fetch11 BAD fake fake fakey fake\r
+EOF
+while read line; do :; done
+EOSCRIPT
+chmod 755 "$imapd"
+
+# TODO plumb this error out to inc
+
+check_exit '-eq 1' inc +imap
+if ! grep -q 'BAD.*fake fake fakey fake' "$MH_TEST_DIR/$profile/imap.d/stderr"; then
+ cat "$MH_TEST_DIR/$profile/imap.d/stderr" >&2
+ fail "BAD response not in $MH_TEST_DIR/$profile/imap.d/stderr"
+fi
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+# Errors: can't live with 'em can't live without 'em
+# - Priorities:
+# 0. db integrity
+# 1. report comprehensible error to user
+# 2. comprehensible log (good enough for a quality bug report)
+# 3. clean close of IMAP connection
+# - If db open:
+# 1. clean close if at all possible
+# 2. if not, report failure along with the original failure
+# - If nmh connection open:
+# 1. write blind error
+# 2. if that write failed log but ignore
+# 3. Else close the socket, log+ignore error here too
+# - If IMAP connection is open:
+# 1. write a blind LOGOUT
+# 2. if that write failed log but ignore
+# 3. Else close the socket, log+ignore error here too
+# - If error is detected at layer that has both nmh and IMAP connection
+# available, handle nmh first; we fight for the users
+# - In any case, finally error!(it)
+# Types:
+# Environmental
+# - read/write error
+# - open error (except ENOENT)
+# Action: report, log, clean shutdown
+# Protocol
+# - either the bytes received were bytes expected or they weren't;
+# context for logging is useful, but we don't need fine-grained types
+# - this goes for nmh-folder-manager and IMAP protocols
+# Action: report, log, clean shutdown
+# Invalid request
+# A request is valid or not regardless of the state of the system.
+# - E.g.
+# - UID FETCH without mailbox open - protocol violation: EXAMINE first
+# - UID FETCH fred - `fred` is not a valid UID
+# - In theory, this maps to IMAP `BAD response code, but can we rely on it?
+# Action: Internal error, below
+# Illegal request
+# A request must be valid before its legality may be judged.
+# - E.g.
+# - FETCH 10 on mailbox with 9 messages - NO such message
+# - EXAMINE nosuchmailbox - NO mailbox by that name exists
+# - In theory, this maps to IMAP `NO response code, but can we rely on it?
+# - These SHOULD only be caused by user action, e.g. create folder that already exists.
+# Action: report, log, NO SHUTDOWN
+# Internal
+# Action: report, log, clean shutdown
+
+testname=${0#$srcdir/}
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+rm -rf "$MH_TEST_DIR/panda"
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+
+ # TODO uh oops don't need two profiles for this one
+ for i in 1 2; do
+ set_mh_profile profile$i
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+ done
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+
+mkdir "$MH_TEST_DIR/panda"
+mkdir "$MH_TEST_DIR/panda/test"
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+start_fakeinetd env "HOME=$MH_TEST_DIR/panda" "$PANDA_IMAPD"
+imapd_pid=$fakeinetd_pid
+
+start() {
+ rm -f "$MH_TEST_DIR/$profile/imap.d/socket"
+ status=0
+ LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw" "$MH_OBJ_DIR/target/debug/mhimap" imap start > "$actual" 2>&1 || status=$?
+}
+
+# setup profile 2 and 1, leaving 1 as active profile
+for i in 2 1; do
+ setup_test_without_data profile$i
+ trap imap_cleanup 0
+ cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/profile$i/imap.d/socket
+imap-db: $MH_TEST_DIR/profile$i/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: PREAUTH
+imap-mailbox-root: test/
+EOF
+ if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ fi
+ start
+done
+
+################################################################################
+start_test 'profile1: folder +imap/fold -create'
+check_exit '-eq 0' folder +imap/fold -create
+
+################################################################################
+start_test 'profile2: folder +imap/fold -create'
+
+set_mh_profile profile2
+
+# TODO Oops folder -create when folder is already known LOCALLY to exist tries to create over IMAP!
+
+cat > "$expected" <<EOF
+folder: failed to create folder imap/fold: Can't create mailbox node $MH_TEST_DIR/panda/test/fold: File exists
+EOF
+
+exitcode=1
+folder +imap/fold -create > "$actual" 2>&1 || exitcode=$?
+check "$actual" "$expected"
+if [ $exitcode -ne 1 ]; then
+ fail "folder exited $exitcode rather than 1"
+fi
+
+################################################################################
+start_test 'profile2: inc +imap leaves imap/fold SELECTed'
+inc +imap
+
+################################################################################
+start_test 'profile1: rmf +imap/fold fails because profile1 has it SELECTED'
+
+set_mh_profile profile1
+
+rmf +imap/fold -nointeractive || status=$?
+if ! grep -q 'NO Mailbox test/fold is in use by another process' "$MH_TEST_DIR/$profile/imap.d/stderr"; then
+ cat "$MH_TEST_DIR/$profile/imap.d/stderr" >&2
+ fail 'mailbox deletion not disallowed'
+fi
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+# TODO this must have bitrotted to oblivion...
+
+testname=${0#$srcdir/}
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+setup_test_without_data Mail
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+# * LIST (\All \HasNoChildren) "/" "[Gmail]/All Mail"
+cat >> "$MH" <<EOF
+oauth-gmail-credential-file: $MH_IMAP_TEST_OAUTH_FILE
+imap-host: imap.gmail.com
+imap-port: imaps
+imap-user: $MH_IMAP_TEST_USER
+imap-tls: implicit
+imap-auth: SASL
+imap-saslmech: xoauth2
+imap-authservice: gmail
+imap-mailbox-root: [Gmail]
+imap-mailbox-exclude: .
+imap-mailbox-include: All Mail
+Unseen-Sequence: unseen
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+start_test 'inc All Mail'
+inc +imap
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+testname=${0#$srcdir/}
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+rm -rf "$MH_TEST_DIR/panda"
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+
+ for profile in $profiles; do
+ set_mh_profile $profile
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+ done
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+
+mkdir "$MH_TEST_DIR/panda"
+mkdir "$MH_TEST_DIR/panda/test"
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+# TODO duplicated with test-scan
+arith_val=1
+message_count=11
+cat > "$MH_TEST_DIR/panda/test/fold" <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: 1603585238 $message_count testseq
+Status: RO
+X-Status:
+X-Keywords:
+X-UID: $arith_val
+
+body
+
+EOF
+
+start_fakeinetd env "HOME=$MH_TEST_DIR/panda" "$PANDA_IMAPD"
+imapd_pid=$fakeinetd_pid
+
+start() {
+ rm -f "$MH_TEST_DIR/$profile/imap.d/socket"
+ status=0
+ LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw" "$MH_OBJ_DIR/target/debug/mhimap" imap start > "$actual" 2>&1 || status=$?
+}
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+
+# setup profile 2 and 1, leaving 1 as active profile
+for i in 3 2 1; do
+ setup_test_without_data profile$i
+ trap imap_cleanup 0
+
+ # TODO Stop using mailbox-root in all but live tests once we have a local
+ # panda hacked never to look at real mailboxes.
+ cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/$profile/imap.d/socket
+imap-db: $MH_TEST_DIR/$profile/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: PREAUTH
+imap-mailbox-root: test/
+imap-mailbox-exclude: excluded
+imap-mailbox-include: excluded/re-included
+EOF
+ if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ fi
+done
+
+# 4 messages in profile 1
+echo 'imap-max-fetch-messages-in-flight: 4' >> "$MH"
+start
+
+################################################################################
+start_test 'inc'
+
+echo 'Incorporating new mail into imap/fold...' > "$expected.inc.full"
+echo >> "$expected.inc.full"
+echo " $arith_val 10/24 me <<body >>" >> "$expected.inc.full"
+
+while [ $arith_val -lt $message_count ]; do
+ arith_eval $arith_val + 1
+ printf ' %2d U 10/24 me <<body >>\n' $arith_val >> "$expected.inc.full"
+ cat >> $MH_TEST_DIR/panda/test/fold <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: $arith_val
+
+body
+
+EOF
+ arith_eval $arith_val + 1
+ printf ' %2d U 10/24 me <<body >>\n' $arith_val >> "$expected.inc.full"
+ cat >> $MH_TEST_DIR/panda/test/fold <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: $arith_val
+
+body
+
+EOF
+done
+
+cp "$expected.inc.full" "$expected.inc"
+
+inc +imap -form scan.unseen -width 80 > "$actual"
+check "$actual" "$expected.inc"
+
+################################################################################
+start_test 'inc after inc finds no new mail'
+
+cat /dev/null > "$expected.inc"
+inc +imap -form scan.unseen -width 80 > "$actual"
+check "$actual" "$expected.inc"
+
+################################################################################
+start_test 'inc after inc finds new mail'
+
+arith_eval $arith_val + 1
+ cat >> $MH_TEST_DIR/panda/test/fold <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: $arith_val
+
+body
+
+EOF
+echo 'Incorporating new mail into imap/fold...' > "$expected.inc"
+echo >> "$expected.inc"
+printf ' %2d U 10/24 me <<body >>\n' $arith_val >> "$expected.inc"
+
+# Kill the folder manager so inc will cause a re-SELECT.
+"$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+inc +imap -form scan.unseen -width 80 > "$actual"
+check "$actual" "$expected.inc"
+
+################################################################################
+start_test 'child of excluded mailbox excluded'
+
+mkdir -p "$MH_TEST_DIR/panda/test/excluded"
+cat > "$MH_TEST_DIR/panda/test/excluded/fold" <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: 1603585238 1 testseq
+Status: RO
+X-Status:
+X-Keywords:
+X-UID: 1
+
+body
+EOF
+
+cat /dev/null > "$expected"
+inc +imap > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'explicitly included child of excluded mailbox included'
+
+cp "$MH_TEST_DIR/panda/test/excluded/fold" "$MH_TEST_DIR/panda/test/excluded/re-included"
+
+cat > "$expected" <<EOF
+Incorporating new mail into imap/excluded/re-included...
+
+ 1 10/24 me <<body >>
+EOF
+inc +imap > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'explicitly requested child of excluded mailbox included'
+
+cp "$MH_TEST_DIR/panda/test/excluded/fold" "$MH_TEST_DIR/panda/test/excluded/still-excluded"
+
+cat > "$expected" <<EOF
+Incorporating new mail into imap/excluded/still-excluded...
+
+ 1 10/24 me <<body >>
+EOF
+inc +imap/excluded/still-excluded > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'fetch messages all fit in first FETCH UID'
+
+set_mh_profile profile2
+
+# 12 messages so go for 11 per fetch.
+# This hits the '1d. no more UIDs to FETCH but we are still expecting FETCH UID responses from that initial fetch' case in do_inc.
+echo 'imap-max-fetch-messages-in-flight: 11' >> "$MH"
+start
+
+(
+ sed 's/ U / /; s| 1 10/24 me| 1 10/24 me|' "$expected.inc.full"
+ echo ' 12 10/24 me <<body >>'
+) > "$expected.inc"
+
+inc +imap/fold > "$actual"
+check "$actual" "$expected.inc"
+
+################################################################################
+start_test '1d. just one more UID to FETCH'
+
+set_mh_profile profile3
+
+cat >> $MH_TEST_DIR/panda/test/fold <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: $arith_val
+
+body
+
+EOF
+
+# max:6 means the first FETCH UID requests 12 leaving just one more for the 2nd FETCH UID.
+echo 'imap-max-fetch-messages-in-flight: 6' >> "$MH"
+start
+
+(
+ sed 's/ U / /; s| 1 10/24 me| 1 10/24 me|' "$expected.inc.full"
+ echo ' 12 10/24 me <<body >>'
+ echo ' 13 10/24 me <<body >>'
+) > "$expected.inc"
+rm -f "$expected.inc.full"
+
+inc +imap/fold > "$actual"
+check "$actual" "$expected.inc"
+
+################################################################################
+start_test 'one message in page 2'
+
+set_mh_profile profile1
+
+# Still using max-fetch-messages-in-flight:4
+# The first FETCH UID requests 1:8 (pages 1 and 2).
+# We provide a mailbox with messages 1-9.
+
+genmailbox() {
+ cat <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: 1626040083 $1
+Status: O
+X-Status:
+X-Keywords:
+X-UID: 1
+
+body
+
+EOF
+ arith_val=2
+ while [ $arith_val -le $1 ]; do
+ cat <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: $arith_val
+
+body
+
+EOF
+ arith_eval $arith_val + 1
+ done
+}
+
+genmailbox 13 > $MH_TEST_DIR/panda/test/omp2
+
+cat > "$expected.inc" <<EOF
+Incorporating new mail into imap/omp2...
+
+ 1 U 10/24 me <<body >>
+ 2 U 10/24 me <<body >>
+ 3 U 10/24 me <<body >>
+ 4 U 10/24 me <<body >>
+ 5 U 10/24 me <<body >>
+ 6 U 10/24 me <<body >>
+ 7 U 10/24 me <<body >>
+ 8 U 10/24 me <<body >>
+ 9 U 10/24 me <<body >>
+ 10 U 10/24 me <<body >>
+ 11 U 10/24 me <<body >>
+ 12 U 10/24 me <<body >>
+ 13 U 10/24 me <<body >>
+EOF
+
+inc +imap/omp2 -form scan.unseen -width 80 > "$actual" || :
+check "$actual" "$expected.inc"
+
+################################################################################
+start_test 'UID gap with one remaining'
+
+set_mh_profile profile3
+
+# Still using max-fetch-messages-in-flight:6
+# The first FETCH UID requests 1:12.
+# We provide a mailbox with a UID gap though; only these are present:
+# 1, 3, 13
+
+cat > $MH_TEST_DIR/panda/test/uidgap1 <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: 1626040083 13
+Status: O
+X-Status:
+X-Keywords:
+X-UID: 1
+
+body
+
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: 3
+
+body
+
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: 13
+
+body
+
+EOF
+
+cat > "$expected.inc" <<EOF
+Incorporating new mail into imap/uidgap1...
+
+ 1 U 10/24 me <<body >>
+ 2 U 10/24 me <<body >>
+ 3 U 10/24 me <<body >>
+EOF
+
+inc +imap/uidgap1 -form scan.unseen -width 80 > "$actual" || :
+check "$actual" "$expected.inc"
+
+################################################################################
+start_test 'UID gap with two remaining'
+
+set_mh_profile profile3
+
+# Still using max-fetch-messages-in-flight:6
+# The first FETCH UID requests 1:12.
+# We provide a mailbox with many UID gaps though; only these are present:
+# 1, 3, 20, 30
+# oh hey! what happens when there's HUGE gaps?!
+# we'll keep making dumb requests for sequences like 13:18 for no gain, as this test demonstrates
+# is there a better way to discover UIDs?
+# even if there is, these tests are worth having
+# nothing obvious. we can move the FLAGS request to the FETCH BODY though
+
+cat > $MH_TEST_DIR/panda/test/uidgap2 <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: 1626040083 30
+Status: O
+X-Status:
+X-Keywords:
+X-UID: 1
+
+body
+
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: 3
+
+body
+
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: 20
+
+body
+
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: 30
+
+body
+
+EOF
+
+cat > "$expected.inc" <<EOF
+Incorporating new mail into imap/uidgap2...
+
+ 1 U 10/24 me <<body >>
+ 2 U 10/24 me <<body >>
+ 3 U 10/24 me <<body >>
+ 4 U 10/24 me <<body >>
+EOF
+
+inc +imap/uidgap2 -form scan.unseen -width 80 > "$actual" || :
+check "$actual" "$expected.inc"
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+# TODO FIX uh oh this and the other missing msg cases passed by accident after inc uses ESEARCH
+# and now switching inc to leave mailbox open has broken it completely!
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+# common.sh sets this to something crazy; did i do that?!
+testname=${0#$srcdir/}
+
+setup_test_without_data Mail
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ rm -f "$imapd"
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+imapd="$MH_TEST_DIR/$$.imapd"
+start_fakeinetd "$imapd"
+imapd_pid=$fakeinetd_pid
+
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: PREAUTH
+imap-mailbox-root: test/
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+start_test 'message disappears between FETCH UID and FETCH BODY'
+# test the case where a message goes missing between the time inc first
+# notices it and when it goes to download it, e.g. inc starts, sees a message,
+# and then another client deletes it before inc can fetch it.
+
+# I'm kinda surprised this just worked on the first try... hmm or not.
+# I see. The ServerData::Fetch loop doesn't know if it's procesinsg a
+# response from our request for a range of UIDS or from our request of a
+# message body.
+# TODO Teach it the difference so we can log when a message goes missing.
+
+# Documented behaviors:
+# https://datatracker.ietf.org/doc/html/rfc3501#section-6.4.8 says:
+# A non-existent unique identifier is ignored without any error
+# message generated. Thus, it is possible for a UID FETCH command
+# to return an OK without any data or a UID COPY or UID STORE to
+# return an OK without performing any operations.
+# This seems to match the Panda behavior I see.
+
+# https://datatracker.ietf.org/doc/html/rfc2180#section-4.1.1 has an example:
+# => B003 FETCH 4:7 RFC822
+# <= B003 NO Messages 4:7 are no longer available.
+# and in 4.1.2:
+# => B001 FETCH 3:5 ENVELOPE
+# <= * 3 FETCH ENVELOPE . . . (ENVELOPE info returned)
+# <= B001 NO Some of the requested messages no longer exist
+# and in 4.1.3:
+# If all of the messages in the subsequent FETCH command have been
+# expunged, the server SHOULD return only a tagged NO.
+# and mayb it can return NIL or empty string for BODY[] ?
+
+# Observed behaviors
+# Panda: no data but just an OK!
+# (tls-encrypted) => fetch1 UID FETCH 101 BODY.PEEK[]
+# (tls-decrypted) <= fetch1 OK UID FETCH completed
+# Dovecot: same
+# => fetch1 UID FETCH 101 BODY.PEEK[]
+# <= fetch1 OK Fetch completed (0.001 + 0.000 secs).
+# Cyrus: same
+# => fetch1 UID FETCH 101 BODY.PEEK[]
+# <= fetch1 OK Completed (0.000 sec)
+# Gandi: same
+# (tls-encrypted) => fetch1 UID FETCH 101 BODY.PEEK[]
+# (tls-decrypted) <= fetch1 OK Fetch completed (0.001 + 0.000 secs).
+# Gmail
+# (tls-encrypted) => fetch1 UID FETCH 101 BODY.PEEK[]
+# (tls-decrypted) <= fetch1 OK Success
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* PREAUTH [CAPABILITY IMAP4REV1 UIDPLUS] authenticated\r
+* LIST (\NoInferiors \UnMarked) "/" test/fold\r
+1 OK LIST completed\r
+* 1 EXISTS\r
+* OK [UIDVALIDITY 1603585238] UID validity status\r
+* OK [UIDNEXT 12] Predicted next UID\r
+1 OK [READ-WRITE] SELECT completed\r
+* 1 FETCH (UID 1 FLAGS (\Seen))\r
+1 OK UID FETCH completed\r
+fetch11 OK UID FETCH completed\r
+1 OK SEARCH ALL completed
+1 OK CLOSE completed\r
+EOF
+while read line; do :; done
+EOSCRIPT
+chmod 755 "$imapd"
+
+check_exit '-eq 0' inc +imap
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+# TODO FIX uh oh this and the other missing msg cases passed by accident after inc uses ESEARCH
+# and now switching inc to leave mailbox open has broken it completely!
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+# common.sh sets this to something crazy; did i do that?!
+testname=${0#$srcdir/}
+
+setup_test_without_data Mail
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ rm -f "$imapd"
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+imapd="$MH_TEST_DIR/$$.imapd"
+start_fakeinetd "$imapd"
+imapd_pid=$fakeinetd_pid
+
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: PREAUTH
+imap-mailbox-root: test/
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+# TODO handle this case next:
+# UIDNEXT 12 so FETCH 1:11 but nothing is returned; 11 was deleted but never seen.
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* PREAUTH [CAPABILITY IMAP4REV1 UIDPLUS] authenticated\r
+* LIST (\NoInferiors \UnMarked) "/" test/fold\r
+1 OK LIST completed\r
+* 1 EXISTS\r
+* OK [UIDVALIDITY 1603585238] UID validity status\r
+* OK [UIDNEXT 12] Predicted next UID\r
+1 OK [READ-WRITE] SELECT completed\r
+1 OK UID FETCH completed\r
+1 OK SEARCH ALL completed
+1 OK CLOSE completed\r
+EOF
+while read line; do :; done
+EOSCRIPT
+chmod 755 "$imapd"
+
+check_exit '-eq 0' inc +imap
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+# Cyrus
+# MH_IMAP_TEST_SERVER=127.0.0.1 MH_IMAP_TEST_PORT=143 MH_IMAP_TEST_USER=test MH_IMAP_TEST_PASSWORD=test MH_IMAP_TEST_EMAIL_ADDRESS=test MH_IMAP_TEST_TLS=no MH_IMAP_TEST_MAILBOX_ROOT=test/ make check TESTS=test/imap/test-live
+# Dovecot
+# MH_IMAP_TEST_SERVER=127.0.0.1 MH_IMAP_TEST_PORT=1043 MH_IMAP_TEST_USER=test MH_IMAP_TEST_PASSWORD=testpw MH_IMAP_TEST_EMAIL_ADDRESS=test MH_IMAP_TEST_TLS=no MH_IMAP_SERVER_EATS_TRAILING_EOL=' ' make check TESTS=test/imap/test-live
+# Panda
+# MH_IMAP_TEST_SERVER=panda make check TESTS=test/imap/test-live
+# Gandi (looks like they use Dovecot)
+# MH_IMAP_TEST_SERVER=mail.gandi.net MH_IMAP_TEST_PORT=imaps MH_IMAP_TEST_USER=... MH_IMAP_TEST_PASSWORD='...' MH_IMAP_TEST_EMAIL_ADDRESS=test@pretzelnet.org MH_IMAP_TEST_TLS=implicit MH_IMAP_TEST_MAILBOX_ROOT=test/ make check TESTS=test/imap/test-live
+# Gmail
+# MH_IMAP_TEST_SERVER=gmail MH_IMAP_TEST_PORT=imaps MH_IMAP_TEST_USER=... MH_IMAP_TEST_OAUTH_FILE="/path/to/oauth-gmail" MH_IMAP_TEST_EMAIL_ADDRESS=... MH_IMAP_TEST_MAILBOX_ROOT=test/ make check TESTS=test/imap/test-live
+
+testname=${0#$srcdir/}
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+if [ -z "$MH_IMAP_TEST_SERVER" ]; then
+ test_skip 'no IMAP test server selected'
+fi
+
+start() {
+ rm -f "$MH_TEST_DIR/$profile/imap.d/socket"
+ status=0
+ LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw" "$MH_OBJ_DIR/target/debug/mhimap" imap start > "$actual" 2>&1 || status=$?
+}
+
+MH_template="$MH_TEST_DIR/template.mh_profile"
+components_template="$MH_TEST_DIR/template.components"
+scan_template="$MH_TEST_DIR/template.scan.nodate"
+
+rm -f "$MH_template" "$components_template" "$scan_template"
+rm -rf "$MH_TEST_DIR/dovecot" "$MH_TEST_DIR/panda"
+
+rm -rf "$MH_TEST_DIR"/*/imap.d
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ if [ -z "$MH_TEST_NOCLEANUP" ]; then
+ for i in $test_folders; do
+ rmf +$i -nointeractive
+ done
+ fi
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+
+ for profile in $profiles; do
+ set_mh_profile $profile
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+ done
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+
+if [ "$MH_IMAP_TEST_SERVER" = 'panda' ]; then
+ # TODO It seems drafts are born unseen on Cyrus and GMail.
+ MH_IMAP_SERVER_DRAFT_BORN_UNSEEN=' '
+ # TODO Why is Cyrus eating the trailing CRLF?
+ # I'm planning to use refile -file instead of comp anyway; will that help?
+ MH_IMAP_SERVER_EATS_TRAILING_EOL=' '
+ [ -z "$LOGNAME" ] && fail 'LOGNAME not set'
+ MH_IMAP_TEST_EMAIL_ADDRESS="$LOGNAME"
+ mkdir "$MH_TEST_DIR/panda"
+ # Thought creating this stopped Panda from looking at the developer's real
+ # inbox, but it doesn't stop it from mangling /var/mail!
+ # TODO Ship customized Panda for testing.
+ touch "$MH_TEST_DIR/panda/mbox"
+ start_fakeinetd env "HOME=$MH_TEST_DIR/panda" "$PANDA_IMAPD"
+ imapd_pid=$fakeinetd_pid
+ cat > "$MH_template" <<EOF
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-auth: PREAUTH
+imap-tls: don't protect my data i know what i'm doing
+imap-mailbox-root: test/
+EOF
+elif [ "$MH_IMAP_TEST_SERVER" = dovecot ]; then
+ # TODO dovecot "-Fc$MH_TEST_DIR/dovecot/cf"
+ # currently starting manually before running tests:
+ # rm -rf .o/std/dovecot && mkdir .o/std/dovecot && dovecot -Fcdocs/imap/testing/dovecot.conf &
+ cat > "$MH_TEST_DIR/dovecot/cf" <<EOF
+base_dir = $MH_TEST_DIR/dovecot
+state_dir = $MH_TEST_DIR/dovecot/db
+log_path = $MH_TEST_DIR/dovecot/log
+
+default_internal_user = $LOGNAME
+default_login_user = $LOGNAME
+default_internal_group = $LOGNAME
+
+mail_location = mbox:$MH_TEST_DIR/dovecot/db/mail/%d/%1n/%n:INDEX=$MH_TEST_DIR/dovecot/db/indexes/%d/%1n/%n
+
+protocols = imap
+listen = 127.0.0.1
+
+passdb {
+ driver = static
+ args = password=test
+}
+
+namespace inbox {
+ inbox = yes
+}
+
+service anvil {
+ chroot =
+}
+
+service imap-login {
+ chroot =
+ inet_listener imap {
+ port = 1043
+ }
+ inet_listener imaps {
+ port = 1044
+ }
+}
+EOF
+elif [ "$MH_IMAP_TEST_SERVER" = 'gmail' ]; then
+ # * LIST (\All \HasNoChildren) "/" "[Gmail]/All Mail"
+ cat > "$MH_template" <<EOF
+oauth-gmail-credential-file: $MH_IMAP_TEST_OAUTH_FILE
+imap-host: imap.gmail.com
+imap-port: imaps
+imap-user: $MH_IMAP_TEST_USER
+imap-tls: implicit
+imap-auth: SASL
+imap-saslmech: xoauth2
+imap-authservice: gmail
+EOF
+ if [ -n "$MH_IMAP_TEST_MAILBOX_ROOT" ]; then
+ echo "imap-mailbox-root: $MH_IMAP_TEST_MAILBOX_ROOT" >> "$MH_template"
+ fi
+elif [ -z "$MH_IMAP_TEST_PORT" ] \
+ || [ -z "$MH_IMAP_TEST_TLS" ] \
+ || [ -z "$MH_IMAP_TEST_USER" ] \
+ || [ -z "$MH_IMAP_TEST_PASSWORD" ] \
+ || [ -z "$MH_IMAP_TEST_EMAIL_ADDRESS" ]; then
+ fail 'MH_IMAP_TEST_PORT MH_IMAP_TEST_USER MH_IMAP_TEST_PASSWORD MH_IMAP_TEST_EMAIL_ADDRESS must all be set'
+else
+ case $MH_IMAP_TEST_TLS in
+ no)
+ MH_IMAP_TEST_TLS="don't protect my data i know what i'm doing"
+ ;;
+ unverified-implicit)
+ MH_IMAP_TEST_TLS="implicit but don't verify the cert i already did out of band"
+ ;;
+ esac
+ cat > "$MH_TEST_DIR/netrc" <<EOF
+machine $MH_IMAP_TEST_SERVER login $MH_IMAP_TEST_USER password $MH_IMAP_TEST_PASSWORD
+EOF
+ cat >> "$MH_template" <<EOF
+credentials: file-nopermcheck:$MH_TEST_DIR/netrc
+imap-host: $MH_IMAP_TEST_SERVER
+imap-port: $MH_IMAP_TEST_PORT
+imap-user: $MH_IMAP_TEST_USER
+imap-auth: SASL
+imap-tls: $MH_IMAP_TEST_TLS
+EOF
+ if [ -n "$MH_IMAP_TEST_MAILBOX_ROOT" ]; then
+ echo "imap-mailbox-root: $MH_IMAP_TEST_MAILBOX_ROOT" >> "$MH_template"
+ fi
+fi
+
+# it seems with cyrus everything has to go under INBOX? i probably
+# messed something up but whatever, let's roll with it.
+# * LIST (\HasChildren) "/" INBOX
+#* LIST (\HasNoChildren) "/" INBOX/foo
+
+imap_folder=imap/imap$(date '+%j%H%M%S')
+
+cat >> "$MH_template" <<EOF
+Unseen-Sequence: unseen
+Local-Mailbox: $MH_IMAP_TEST_EMAIL_ADDRESS
+Draft-Folder: $imap_folder/drafts
+Managed-Folders: imap
+rmm: -unlink
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+EOF
+
+cat > "$scan_template" <<EOF
+%4(msg)%<(cur)+%| %>%<(unseen)U%| %>%<{replied}-%| %>\
+%<(mymbox{from})%<{to}To:%(unquote(decode(friendly{to})))%>%>\
+%<(zero)%(unquote(decode(friendly{from})))%> \
+%(decode{subject})%<{body}<<%{body}>>%>
+EOF
+
+# TODO default components use '--------' to close header...
+cat > "$components_template" <<EOF
+%<{from}%|%(void(localmbox))%>%(void(width))%(putaddr From: )
+%<{to}%(void(width))%(putaddr To: )%|To:%>
+%<{cc}%(void(width))%(putaddr cc: )%|cc:%>
+Fcc: %<{fcc}%(putstr)%|+outbox%>
+Subject:%<{subject} %(putstr)%>
+
+body
+EOF
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+################################################################################
+profile=profile1
+rm -rf "$MH_TEST_DIR/$profile"
+setup_test_without_data $profile
+trap imap_cleanup 0
+
+cat "$MH_template" >> "$MH"
+cp "$components_template" "$MH_TEST_DIR/$profile/components"
+cp "$scan_template" "$MH_TEST_DIR/$profile/scan.nodate"
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+# Set this for the EHLO command
+echo "clientname: nosuchhost.example.com" >> "$MHMTSCONF"
+
+cat >> "$MH" <<EOF
+imap-socket: $MH_TEST_DIR/$profile/imap.d/socket
+imap-db: $MH_TEST_DIR/$profile/imap.d
+EOF
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+
+start
+################################################################################
+
+################################################################################
+start_test 'folder -create'
+
+cat > "$expected" <<EOF
+$imap_folder/drafts+ has no messages.
+$imap_folder/sent+ has no messages.
+EOF
+test_folders="$imap_folder/drafts $imap_folder/sent"
+(run_prog folder +$imap_folder/drafts -create \
+ && run_prog folder +$imap_folder/sent -create) > "$actual"
+check "$actual" "$expected"
+
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/drafts
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/sent
+
+################################################################################
+start_test 'inc when nothing to fetch'
+
+cat /dev/null > "$expected"
+check_exit '-eq 0' inc +imap -width 80
+
+################################################################################
+start_test 'comp creates draft in empty draft folder'
+
+check_exit '-eq 0' comp -noedit -to "$MH_IMAP_TEST_EMAIL_ADDRESS" -fcc $imap_folder/sent -subject 'testing imap' -nowhatnowproc
+
+# comp creates a message, stores a UID/msgnum pair, runs the user's editor,
+# creates a new message with the user's edits, and replaces the UID/msgnum pair.
+# So verify the integrity of the database after that.
+# TODO any other operations worth having this integrity check on?
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/drafts
+
+start_test 'scan drafts after comp'
+cat > "$expected" <<EOF
+ 1 To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap<<body >>
+EOF
+run_prog scan -form scan.nodate +$imap_folder/drafts -width 80 > "$actual"
+check "$actual" "$expected"
+
+start_test 'show draft after comp'
+cat > "$expected" <<EOF
+(Message $imap_folder/drafts:1)
+
+To: $MH_IMAP_TEST_EMAIL_ADDRESS
+cc:
+From: $MH_IMAP_TEST_EMAIL_ADDRESS
+Subject: testing imap
+
+Fcc: $imap_folder/sent
+
+body
+EOF
+run_prog show +$imap_folder/drafts 1 > "$actual"
+check "$actual" "$expected"
+
+start_test 'check .mh_sequences after show after comp'
+cat > "$expected" <<EOF
+cur: 1
+EOF
+check "$MH_TEST_DIR/$profile/$imap_folder/drafts/.mh_sequences" "$expected" 'keep first'
+
+################################################################################
+start_test 'comp creates draft in non-empty draft folder'
+
+check_exit '-eq 0' comp -noedit -to "$MH_IMAP_TEST_EMAIL_ADDRESS" -fcc $imap_folder/sent -subject 'testing imap 2' -nowhatnowproc
+
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/drafts
+
+start_test 'scan non-empty drafts after comp'
+cat > "$expected" <<EOF
+ 1+ To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap<<body >>
+ 2 To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap 2<<body >>
+EOF
+run_prog scan -form scan.nodate +$imap_folder/drafts -width 80 > "$actual"
+check "$actual" "$expected"
+
+start_test 'check .mh_sequences after non-empty comp'
+cat > "$expected" <<EOF
+nmh-picked: 1-2
+cur: 1
+EOF
+check "$MH_TEST_DIR/$profile/$imap_folder/drafts/.mh_sequences" "$expected" 'keep first'
+
+################################################################################
+start_test 'send refiles draft to fcc folder'
+
+arith_eval 64000 + $$ % 1000
+localport=$arith_val
+"$MH_OBJ_DIR/test/fakesmtp" /dev/null $localport > /dev/null
+check_exit '-eq 0' send -server 127.0.0.1 -port $localport -draftfolder $imap_folder/drafts
+rm -f "$actual"
+
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/drafts
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/sent
+
+# TODO what?! this test started out as this and evolved into something else?
+start_test 'scan drafts finds no messages after send'
+
+# Check draft moved to FCC folder
+cat > "$expected" <<EOF
+ 2 To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap 2<<body >>
+EOF
+run_prog scan -form scan.nodate +$imap_folder/drafts -width 80 > "$actual"
+check "$actual" "$expected"
+
+start_test 'scan sent finds the sent messages after send'
+
+cat > "$expected" <<EOF
+ 1 To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap<<body >>
+EOF
+run_prog scan -form scan.nodate +$imap_folder/sent -width 80 > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+profile=profile2
+rm -rf "$MH_TEST_DIR/$profile"
+setup_test_without_data $profile
+trap imap_cleanup 0
+
+cat "$MH_template" >> "$MH"
+cp "$components_template" "$MH_TEST_DIR/$profile/components"
+cp "$scan_template" "$MH_TEST_DIR/$profile/scan.nodate"
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+cat >> "$MH" <<EOF
+imap-socket: $MH_TEST_DIR/$profile/imap.d/socket
+imap-db: $MH_TEST_DIR/$profile/imap.d
+EOF
+
+start
+################################################################################
+
+################################################################################
+start_test 'inc on empty managed folder fetches all mail in that folder'
+
+cat > "$expected" <<EOF
+Incorporating new mail into $imap_folder/drafts...
+
+ 1 $MH_IMAP_SERVER_DRAFT_BORN_UNSEEN To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap 2<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+Incorporating new mail into $imap_folder/sent...
+
+ 1 To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+EOF
+
+inc +imap -form scan.nodate -width 80 > "$actual"
+check "$actual" "$expected"
+
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/drafts
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/sent
+
+################################################################################
+start_test 'show the sent message after inc'
+
+cat > "$expected" <<EOF
+(Message $imap_folder/sent:1)
+
+Date:
+To: $MH_IMAP_TEST_EMAIL_ADDRESS
+From: $MH_IMAP_TEST_EMAIL_ADDRESS
+Subject: testing imap
+
+MIME-Version: 1.0\r
+
+\r
+EOF
+if [ -n "$MH_IMAP_SERVER_EATS_TRAILING_EOL" ]; then
+ echo 'body\r' >> "$expected"
+else
+ echo -n body >> "$expected"
+fi
+
+run_prog show +$imap_folder/sent 1 > "$actual"
+sed -e 's/^Date:.*/Date:/' "$actual" > "$actual".nodate
+rm -f "$actual"
+check "$actual.nodate" "$expected"
+
+################################################################################
+start_test 'mark the sent message unseen'
+
+check_exit '-eq 0' mark -add -seq unseen +$imap_folder/sent 1
+
+start_test 'scan after mark unseen'
+
+cat > "$expected" <<EOF
+ 1+U To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+EOF
+run_prog scan -form scan.nodate -width 80 > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'show the now-unseen message'
+
+check_exit '-eq 0' show all
+
+start_test 'scan after show'
+
+cat > "$expected" <<EOF
+ 1+ To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+EOF
+run_prog scan -form scan.nodate -width 80 > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'refile -file'
+
+refiled_file="$MH_TEST_DIR/$$.refiled-file"
+cat > "$refiled_file" <<EOF
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+
+refile-test
+EOF
+check_exit '-eq 0' refile -file "$refiled_file" +$imap_folder/drafts
+if [ -e "$refiled_file" ]; then
+ rm -f "$refiled_file" || :
+ fail "$refiled_file missing after refile"
+fi
+
+start_test 'scan dst folder after refile -file finds refiled message'
+
+cat > "$expected" <<EOF
+ 1 To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap 2<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+ 2 me <<refile-test >>
+EOF
+scan -form scan.nodate -width 80 +$imap_folder/drafts > "$actual"
+check "$actual" "$expected"
+
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/drafts
+
+################################################################################
+start_test 'refile'
+
+check_exit '-eq 0' refile -src +$imap_folder/drafts 2 +$imap_folder/sent
+
+# TODO what?! this test started out as this and evolved into something else?
+start_test 'scan src folder after refile finds no messages'
+
+cat > "$expected" <<EOF
+ 1 To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap 2<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+EOF
+scan -form scan.nodate -width 80 +$imap_folder/drafts > "$actual"
+check "$actual" "$expected"
+
+start_test 'scan dst folder after refile finds refiled message'
+
+cat > "$expected" <<EOF
+ 1+ To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+ 2 me <<refile-test >>
+EOF
+scan -form scan.nodate -width 80 +$imap_folder/sent > "$actual"
+check "$actual" "$expected"
+
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/drafts
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/sent
+
+################################################################################
+start_test 'mark unseen after refile'
+
+# This tests the UID update in the local database.
+
+check_exit '-eq 0' mark -add -seq unseen 2
+cat > "$expected" <<EOF
+ 1+ To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+ 2 U me <<refile-test >>
+EOF
+scan -form scan.nodate -width 80 +$imap_folder/sent > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'show all before rmm'
+
+cat > "$expected" <<EOF
+>>> $imap_folder/sent:1
+
+
+Date:
+To: $MH_IMAP_TEST_EMAIL_ADDRESS
+From: $MH_IMAP_TEST_EMAIL_ADDRESS
+Subject: testing imap
+
+MIME-Version: 1.0\r
+
+\r
+EOF
+if [ -n "$MH_IMAP_SERVER_EATS_TRAILING_EOL" ]; then
+ echo 'body\r' >> "$expected"
+else
+ echo -n body >> "$expected"
+fi
+cat >> "$expected" <<EOF
+
+
+
+>>> $imap_folder/sent:2
+
+
+Date:
+To: you
+From: me
+
+
+EOF
+# TODO Why is this one unaffected?! Really need to get to the bottom of this...
+if [ -n "$MH_IMAP_SERVER_EATS_TRAILING_EOL" ]; then
+ echo refile-test >> "$expected"
+else
+ echo refile-test >> "$expected"
+fi
+
+run_prog show +$imap_folder/sent all > "$actual"
+sed -e 's/^Date:.*/Date:/' "$actual" > "$actual.nodate"
+rm -f "$actual"
+check "$actual.nodate" "$expected"
+
+start_test 'check .mh_sequences after show before rmm'
+cat > "$expected" <<EOF
+cur: 2
+EOF
+check "$MH_TEST_DIR/$profile/$imap_folder/sent/.mh_sequences" "$expected" 'keep first'
+
+################################################################################
+start_test 'show cur shows 2 after show all'
+
+cat > "$expected" <<EOF
+(Message $imap_folder/sent:2)
+
+Date: Sat, 24 Oct 2020 19:20:38 -0500
+To: you
+From: me
+
+
+EOF
+# TODO Why is this one unaffected?! Really need to get to the bottom of this...
+if [ -n "$MH_IMAP_SERVER_EATS_TRAILING_EOL" ]; then
+ echo refile-test >> "$expected"
+else
+ echo refile-test >> "$expected"
+fi
+
+run_prog show +$imap_folder/sent > "$actual"
+check "$actual" "$expected"
+
+start_test 'check .mh_sequences after show cur shows 2 after show all'
+cat > "$expected" <<EOF
+cur: 2
+EOF
+check "$MH_TEST_DIR/$profile/$imap_folder/sent/.mh_sequences" "$expected" 'keep first'
+
+################################################################################
+start_test 'show 1 before rmm'
+
+cat > "$expected" <<EOF
+(Message $imap_folder/sent:1)
+
+Date:
+To: $MH_IMAP_TEST_EMAIL_ADDRESS
+From: $MH_IMAP_TEST_EMAIL_ADDRESS
+Subject: testing imap
+
+MIME-Version: 1.0\r
+
+\r
+EOF
+if [ -n "$MH_IMAP_SERVER_EATS_TRAILING_EOL" ]; then
+ echo 'body\r' >> "$expected"
+else
+ echo -n body >> "$expected"
+fi
+
+run_prog show +$imap_folder/sent 1 > "$actual"
+sed -e 's/^Date:.*/Date:/' "$actual" > "$actual".nodate
+rm -f "$actual"
+check "$actual.nodate" "$expected"
+
+start_test 'check .mh_sequences after show 1 before rmm'
+cat > "$expected" <<EOF
+cur: 1
+EOF
+check "$MH_TEST_DIR/$profile/$imap_folder/sent/.mh_sequences" "$expected" 'keep first'
+
+################################################################################
+start_test 'show cur shows 1 after show 1'
+
+cat > "$expected" <<EOF
+(Message $imap_folder/sent:1)
+
+Date:
+To: $MH_IMAP_TEST_EMAIL_ADDRESS
+From: $MH_IMAP_TEST_EMAIL_ADDRESS
+Subject: testing imap
+
+MIME-Version: 1.0\r
+
+\r
+EOF
+if [ -n "$MH_IMAP_SERVER_EATS_TRAILING_EOL" ]; then
+ echo 'body\r' >> "$expected"
+else
+ echo -n body >> "$expected"
+fi
+
+run_prog show +$imap_folder/sent > "$actual"
+sed -e 's/^Date:.*/Date:/' "$actual" > "$actual".nodate
+rm -f "$actual"
+check "$actual.nodate" "$expected"
+
+start_test 'check .mh_sequences after show cur shows 1 after show 1'
+cat > "$expected" <<EOF
+cur: 1
+EOF
+check "$MH_TEST_DIR/$profile/$imap_folder/sent/.mh_sequences" "$expected" 'keep first'
+
+################################################################################
+start_test 'rmm'
+
+check_exit '-eq 0' rmm -unlink 2
+
+start_test 'scan after rmm'
+
+cat > "$expected" <<EOF
+ 1+ To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+EOF
+scan -form scan.nodate -width 80 +$imap_folder/sent > "$actual"
+check "$actual" "$expected"
+
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $imap_folder/sent
+
+################################################################################
+start_test 'inc finds messages to delete 1. create messages in profile2'
+
+for i in 2 3 4 5 6; do
+ refiled_file="$MH_TEST_DIR/$$.refiled-file"
+ cat > "$refiled_file" <<EOF
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+subject: refile $i
+
+refile-test $i
+EOF
+ check_exit '-eq 0' refile -file "$refiled_file" +$imap_folder/drafts
+ if [ -e "$refiled_file" ]; then
+ rm -f "$refiled_file" || :
+ fail "$refiled_file missing after refile"
+ fi
+done
+
+cat > "$expected" <<EOF
+ 1 To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap 2<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+ 2 me refile 2<<refile-test 2 >>
+ 3 me refile 3<<refile-test 3 >>
+ 4 me refile 4<<refile-test 4 >>
+ 5 me refile 5<<refile-test 5 >>
+ 6 me refile 6<<refile-test 6 >>
+EOF
+scan -form scan.nodate -width 80 +$imap_folder/drafts > "$actual"
+check "$actual" "$expected"
+
+for i in $test_folders; do
+ check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $i
+done
+
+################################################################################
+start_test 'inc finds messages to delete 2. inc new messages into profile1'
+
+set_mh_profile profile1
+
+# Message numbers are one greater in profile1 than in profile2.
+cat > "$expected" <<EOF
+Incorporating new mail into $imap_folder/drafts...
+
+ 3 me refile 2<<refile-test 2$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+ 4 me refile 3<<refile-test 3$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+ 5 me refile 4<<refile-test 4$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+ 6 me refile 5<<refile-test 5$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+ 7 me refile 6<<refile-test 6$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+EOF
+inc +imap -form scan.nodate -width 80 > "$actual" || :
+check "$actual" "$expected"
+
+################################################################################
+start_test 'inc finds messages to delete 3. rmm messages out of profile2'
+
+set_mh_profile profile2
+
+check_exit '-eq 0' rmm -unlink 2
+check_exit '-eq 0' rmm -unlink 4
+check_exit '-eq 0' rmm -unlink 5
+
+cat > "$expected" <<EOF
+ 1 To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap 2<<body$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+ 3 me refile 3<<refile-test 3 >>
+ 6 me refile 6<<refile-test 6 >>
+EOF
+scan -form scan.nodate -width 80 +$imap_folder/drafts > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'inc finds messages to delete 4. inc deletes from profile1'
+
+set_mh_profile profile1
+
+inc +imap -form scan.nodate -width 80 > "$actual" || :
+check /dev/null "$actual" 'keep first'
+
+################################################################################
+start_test 'inc finds messages to delete 5. scan profile1 to see messages gone'
+
+# Message numbers are one greater in profile1 than in profile2.
+cat > "$expected" <<EOF
+ 2 To:$MH_IMAP_TEST_EMAIL_ADDRESS testing imap 2<<body >>
+ 4 me refile 3<<refile-test 3$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+ 7 me refile 6<<refile-test 6$MH_IMAP_SERVER_EATS_TRAILING_EOL>>
+EOF
+scan -form scan.nodate -width 80 +$imap_folder/drafts > "$actual"
+check "$actual" "$expected"
+
+# TODO tests:
+# - refile old message into a folder with newer messages: UID validity preserved?
+
+################################################################################
+start_test 'check all folder databases one last time'
+
+for profile in $profiles; do
+ set_mh_profile $profile
+ for i in $test_folders; do
+ check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check $i
+ done
+done
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+testname=${0#$srcdir/}
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+if [ $(uname) != FreeBSD ]; then
+ test_skip 'imap/test-login only working on FreeBSD for now'
+fi
+
+# common.sh sets this to something crazy; did i do that?!
+testname=${0#$srcdir/}
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ rm -f "$imapd" "$actual"
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+
+cat > "$MH_TEST_DIR/netrc" <<EOF
+machine 127.0.0.1 login testuser password password
+EOF
+
+actual="$MH_TEST_DIR/$$.actual"
+
+imapd="$MH_TEST_DIR/$$.imapd"
+
+arith_val=0
+
+start() {
+ rm -f "$MH_TEST_DIR/$profile/imap.d/socket"
+ status=0
+ LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw" "$MH_OBJ_DIR/target/debug/mhimap" imap start > "$actual" 2>&1 || status=$?
+}
+
+################################################################################
+# Tests with nothing even listening.
+
+start_test 'connection refused, explicit TLS'
+
+arith_eval $arith_val + 1
+setup_test_without_data profile$arith_val
+trap imap_cleanup 0
+
+cat >> "$MH" <<EOF
+credentials: file-nopermcheck:$MH_TEST_DIR/netrc
+Managed-Folders: imap
+imap-log-level: error
+imap-log-error-to: $MH_TEST_DIR/error
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/$profile/imap.d/socket
+imap-db: $MH_TEST_DIR/$profile/imap.d
+imap-host: 127.0.0.1
+imap-mailbox-root: test/
+imap-port: 255
+imap-tls: explicit but don't verify the cert i already did out of band
+imap-auth: SASL
+imap-user: testuser
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+start
+if ! grep -q 'Connection to "127.0.0.1:255" failed: Connection refused' "$actual"; then
+ cat "$actual" >&2
+ fail 'ECONN not reported'
+fi
+
+# Use fakeinetd for tests of the parts of explicit TLS handling before TLS negotiation.
+start_fakeinetd "$imapd"
+imapd_pid=$fakeinetd_pid
+
+arith_eval $arith_val + 1
+setup_test_without_data profile$arith_val
+trap imap_cleanup 0
+
+cat >> "$MH" <<EOF
+credentials: file-nopermcheck:$MH_TEST_DIR/netrc
+Managed-Folders: imap
+imap-log-level: error
+imap-log-error-to: $MH_TEST_DIR/error
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/$profile/imap.d/socket
+imap-db: $MH_TEST_DIR/$profile/imap.d
+imap-host: 127.0.0.1
+imap-mailbox-root: test/
+imap-port: $fakeinetd_port
+imap-tls: explicit but don't verify the cert i already did out of band
+imap-auth: SASL
+imap-user: testuser
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+################################################################################
+start_test 'rudely close pre-greeting, explicit TLS'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+exit 1
+EOSCRIPT
+chmod 755 "$imapd"
+
+start
+if ! grep -q 'Received EOF on network read' "$actual"; then
+ cat "$actual" >&2
+ fail 'EOF error not reported'
+fi
+
+################################################################################
+start_test 'protocol violation in greeting, explicit TLS'
+
+# Greets with simply "trash\r\n".
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+trash\r
+EOF
+while read line; do :; done
+EOSCRIPT
+
+start
+if ! grep -q 'protocol error: no space in response' "$actual"; then
+ cat "$actual" >&2
+ fail 'protocol error not reported'
+fi
+
+################################################################################
+start_test 'STARTTLS capability not offered'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* OK [CAPABILITY IMAP4REV1 AUTH=PLAIN UIDPLUS]\r
+EOF
+while read line; do :; done
+EOSCRIPT
+
+start
+if ! grep -q "server doesn't offer STARTTLS" "$actual"; then
+ cat "$actual" >&2
+ fail 'missing STARTTLS not reported'
+fi
+
+################################################################################
+start_test 'STARTTLS capability not offered after capabilities requested'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* OK greeting
+1 OK [CAPABILITY IMAP4REV1 AUTH=PLAIN UIDPLUS] CAPABILITY response\r
+EOF
+while read line; do :; done
+EOSCRIPT
+
+start
+if ! grep -q "server doesn't offer STARTTLS" "$actual"; then
+ cat "$actual" >&2
+ fail 'missing STARTTLS not reported'
+fi
+
+################################################################################
+start_test 'rudely closed after STARTTLS'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* OK [CAPABILITY IMAP4REV1 STARTTLS] greeting\r
+EOF
+read line
+EOSCRIPT
+
+start
+if ! grep -q 'Received EOF on network read' "$actual"; then
+ cat "$actual" >&2
+ fail 'EOF not reported'
+fi
+
+################################################################################
+start_test 'trash response to STARTTLS'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* OK [CAPABILITY IMAP4REV1 STARTTLS] greeting\r
+trash\r
+EOF
+while read line; do :; done
+EOSCRIPT
+
+start
+if ! grep -q 'protocol error: no space in response' "$actual"; then
+ cat "$actual" >&2
+ fail 'protocol error not reported'
+fi
+
+kill $imapd_pid
+imapd_pid=
+
+################################################################################
+start_test 'connection refused, implicit TLS'
+
+arith_eval $arith_val + 1
+setup_test_without_data profile$arith_val
+trap imap_cleanup 0
+
+cat >> "$MH" <<EOF
+credentials: file-nopermcheck:$MH_TEST_DIR/netrc
+Managed-Folders: imap
+imap-log-level: error
+imap-log-error-to: $MH_TEST_DIR/error
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/$profile/imap.d/socket
+imap-db: $MH_TEST_DIR/$profile/imap.d
+imap-host: 127.0.0.1
+imap-mailbox-root: test/
+imap-port: 255
+imap-tls: implicit but don't verify the cert i already did out of band
+imap-auth: SASL
+imap-user: testuser
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+start
+if ! grep -q 'Connection to "127.0.0.1:255" failed: Connection refused' "$actual"; then
+ cat "$actual" >&2
+ fail 'ECONN not reported'
+fi
+
+################################################################################
+# Start the server for the rest.
+
+start_stunnel imap "$imapd"
+
+arith_eval $arith_val + 1
+setup_test_without_data profile$arith_val
+trap imap_cleanup 0
+
+cat >> "$MH" <<EOF
+credentials: file-nopermcheck:$MH_TEST_DIR/netrc
+Managed-Folders: imap
+imap-log-level: error
+imap-log-error-to: $MH_TEST_DIR/error
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/$profile/imap.d/socket
+imap-db: $MH_TEST_DIR/$profile/imap.d
+imap-host: 127.0.0.1
+imap-mailbox-root: test/
+imap-port: $fakeinetd_port
+imap-tls: implicit but don't verify the cert i already did out of band
+imap-auth: SASL
+imap-user: testuser
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+################################################################################
+start_test 'rudely close pre-greeting, implicit TLS'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+exit 1
+EOSCRIPT
+
+start
+if ! grep -q 'TLS peer closed remote connection' "$actual"; then
+ cat "$actual" >&2
+ fail 'EOF error not reported'
+fi
+
+################################################################################
+start_test 'no SASL-IR capability'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* OK [CAPABILITY IMAP4REV1 AUTH=PLAIN]\r
+EOF
+while read line; do :; done
+EOSCRIPT
+
+start
+if ! grep -q "server doesn't offer SASL-IR; let us know and we'll support this configuration" "$actual"; then
+ cat "$actual" >&2
+ fail 'missing SASL-IR not reported'
+fi
+
+################################################################################
+start_test 'no UIDPLUS capability'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* OK [CAPABILITY IMAP4REV1 SASL-IR AUTH=PLAIN] greeting\r
+1 OK [CAPABILITY IMAP4REV1] AUTENTICATE response
+EOF
+while read line; do :; done
+EOSCRIPT
+
+start
+if ! grep -q "server doesn't offer required capability UIDPLUS" "$actual"; then
+ cat "$actual" >&2
+ fail 'missing UIDPLUS not reported'
+fi
+
+################################################################################
+start_test 'protocol violation in greeting, implicit TLS'
+
+# Greets with simply "trash\r\n".
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+trash\r
+EOF
+while read line; do :; done
+EOSCRIPT
+
+start
+if ! grep -q 'protocol error: no space in response' "$actual"; then
+ cat "$actual" >&2
+ fail 'protocol error not reported'
+fi
+
+################################################################################
+start_test 'protocol violation in AUTHENTICATE response'
+
+# Responds to the AUTHENTICATE command with simply "trash\r\n".
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* OK [CAPABILITY IMAP4REV1 SASL-IR AUTH=PLAIN] greeting\r
+trash\r
+EOF
+while read line; do :; done
+EOSCRIPT
+
+start
+if ! grep -q 'protocol error: no space in response' "$actual"; then
+ cat "$actual" >&2
+ fail 'protocol error not reported'
+fi
+
+finish_test
+exit $failed
--- /dev/null
+#!/bin/sh
+
+testname=${0#$srcdir/}
+
+# setup copied from test-scan
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+setup_test_without_data Mail
+
+rm -rf "$MH_TEST_DIR/panda"
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+start_fakeinetd env "HOME=$MH_TEST_DIR/panda" "$PANDA_IMAPD"
+imapd_pid=$fakeinetd_pid
+
+# TODO imap-manager: $MH_LIBEXEC_DIR/folder-managers/imap
+# TODO imap-socket: relative path
+# TODO can we come up with reasonable default of socket path?
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-auth: PREAUTH
+imap-tls: don't protect my data i know what i'm doing
+imap-mailbox-root: test/
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+mkdir "$MH_TEST_DIR/panda"
+mkdir "$MH_TEST_DIR/panda/test"
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+cat > "$MH_TEST_DIR/panda/test/folder" <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: 1603585238 11
+Status: RO
+X-Status:
+X-Keywords:
+X-UID: 11
+
+body
+
+EOF
+
+start_test 'inc'
+check_exit '-eq 0' inc +imap
+
+################################################################################
+start_test 'mark -seq unseen'
+
+cat > "$expected" <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: 1603585238 11
+Status: O
+X-Status:
+X-Keywords:
+X-UID: 11
+
+body
+
+EOF
+
+check_exit '-eq 0' mark -add -seq unseen +imap/folder 1
+check "$MH_TEST_DIR/panda/test/folder" "$expected"
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+# Test illegal or questionable behaviors of GMail.
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "${srcdir}/test/oauth/common.sh"
+
+# common.sh sets this to something crazy; did i do that?!
+testname=${0#$srcdir/}
+
+setup_test_without_data Mail
+
+setup_service
+fake_creds <<EOF
+access-testuser: test-access
+refresh-testuser: test-refresh
+expire-testuser: 2000000000
+EOF
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ rm -f "$imapd"
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+imapd="$MH_TEST_DIR/$$.imapd"
+start_fakeinetd "$imapd"
+imapd_pid=$fakeinetd_pid
+
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: SASL
+imap-user: testuser
+imap-saslmech: xoauth2
+imap-authservice: test
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+# It's the "* OK [HIGHESTMODSEQ 8869]" that's illegal: a space is required after the close bracket.
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* OK Gimap ready for requests\r
+* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH2 AUTH=PLAIN AUTH=PLAIN-CLIENTTOKEN AUTH=OAUTHBEARER AUTH=XOAUTH\r
+1 OK capability response\r
+* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- SPECIAL-USE APPENDLIMIT=35651584\r
+1 OK authenticate response\r
+1 OK Success\r
+* FLAGS (\Answered \Flagged \Draft \Deleted \Seen $NotPhishing $Phishing)\r
+* OK [PERMANENTFLAGS (\Answered \Flagged \Draft \Deleted \Seen $NotPhishing $Phishing \*)] Flags permitted.\r
+* OK [UIDVALIDITY 62] UIDs valid.\r
+* 0 EXISTS\r
+* 0 RECENT\r
+* OK [UIDNEXT 1] Predicted next UID.\r
+* OK [HIGHESTMODSEQ 8869]\r
+1 OK [READ-WRITE] imap/foo selected. (Success)\r
+1 OK Returned to authenticated state. (Success)\r
+EOF
+while read line; do :; done
+EOSCRIPT
+chmod 755 "$imapd"
+
+folder -create +imap/foo
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+# TODO this must have bitrotted to oblivion...
+
+testname=${0#$srcdir/}
+
+# setup copied from test-scan
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+setup_test_without_data Mail
+
+rm -rf "$MH_TEST_DIR/panda"
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+start_fakeinetd env "HOME=$MH_TEST_DIR/panda" "$PANDA_IMAPD"
+imapd_pid=$fakeinetd_pid
+
+# TODO imap-manager: $MH_LIBEXEC_DIR/folder-managers/imap
+# TODO imap-socket: relative path
+# TODO can we come up with reasonable default of socket path?
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-auth: PREAUTH
+imap-tls: don't protect my data i know what i'm doing
+imap-mailbox-root: test/
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+mkdir "$MH_TEST_DIR/panda"
+mkdir "$MH_TEST_DIR/panda/test"
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+for i in 1 2 3; do
+ cat > "$MH_TEST_DIR/panda/test/fold$i" <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: 1603585238 1
+Status: RO
+X-Status:
+X-Keywords:
+X-UID: 1
+
+initial message in fold$i
+
+EOF
+done
+
+start_test 'initial inc'
+
+cat > "$expected.inc" <<EOF
+Incorporating new mail into imap/fold1...
+
+ 1 10/24 me <<initial message in fold1 >>
+Incorporating new mail into imap/fold2...
+
+ 1 10/24 me <<initial message in fold2 >>
+Incorporating new mail into imap/fold3...
+
+ 1 10/24 me <<initial message in fold3 >>
+EOF
+
+inc +imap > "$actual"
+check "$actual" "$expected.inc"
+
+################################################################################
+start_test 'refile'
+
+check_exit '-eq 0' refile -src +imap/fold1 1 +imap/fold2
+
+start_test 'scan src folder after refile finds no messages'
+
+cat > "$expected" <<EOF
+scan: no messages match specification
+EOF
+scan -width 80 +imap/fold1 > "$actual" 2>&1 || :
+check "$actual" "$expected"
+
+start_test 'scan dst folder after refile finds refiled message'
+
+cat > "$expected" <<EOF
+ 1 10/24 me <<initial message in fold2 >>
+ 2+ 10/24 me <<initial message in fold1 >>
+EOF
+scan -width 80 +imap/fold2 > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'refile -link -file'
+
+refiled_file="$MH_TEST_DIR/$$.refiled-file"
+cat > "$refiled_file" <<EOF
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+
+refile-link file
+EOF
+check_exit '-eq 0' refile -link -file "$refiled_file" +imap/fold1
+if [ -e "$refiled_file" ]; then
+ rm "$refiled_file"
+else
+ fail "$refiled_file missing after refile -link"
+fi
+
+start_test 'scan dst folder after refile -link -file finds refiled message'
+
+cat > "$expected" <<EOF
+ 1+ 10/24 me <<refile-link file >>
+EOF
+scan -width 80 +imap/fold1 > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'refile -file'
+
+refiled_file="$MH_TEST_DIR/$$.refiled-file"
+cat > "$refiled_file" <<EOF
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+
+refiled file
+EOF
+check_exit '-eq 0' refile -file "$refiled_file" +imap/fold3
+if [ -e "$refiled_file" ]; then
+ rm -f "$refiled_file" || :
+ fail "$refiled_file missing after refile"
+fi
+
+start_test 'scan dst folder after refile -file finds refiled message'
+
+cat > "$expected" <<EOF
+ 1 10/24 me <<initial message in fold3 >>
+ 2+ 10/24 me <<refiled file >>
+EOF
+scan -width 80 +imap/fold3 > "$actual"
+check "$actual" "$expected"
+
+################################################################################
+start_test 'rebuild local from scratch'
+
+rm -rf "$MH_TEST_DIR/Mail/imap" "$MH_TEST_DIR/Mail/imap.d/imap"
+
+cat > "$expected.inc" <<EOF
+Incorporating new mail into imap/fold1...
+
+ 1 10/24 me <<refile-link file >>
+Incorporating new mail into imap/fold2...
+
+ 1 10/24 me <<initial message in fold2 >>
+ 2 10/24 me <<initial message in fold1 >>
+Incorporating new mail into imap/fold3...
+
+ 1 10/24 me <<initial message in fold3 >>
+ 2 10/24 me <<refiled file >>
+EOF
+
+inc +imap > "$actual"
+check "$actual" "$expected.inc"
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+testname=${0#$srcdir/}
+
+# setup copied from test-scan
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+setup_test_without_data Mail
+
+rm -rf "$MH_TEST_DIR/panda"
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+start_fakeinetd env "HOME=$MH_TEST_DIR/panda" "$PANDA_IMAPD"
+imapd_pid=$fakeinetd_pid
+
+# TODO imap-manager: $MH_LIBEXEC_DIR/folder-managers/imap
+# TODO imap-socket: relative path
+# TODO can we come up with reasonable default of socket path?
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-auth: PREAUTH
+imap-tls: don't protect my data i know what i'm doing
+imap-mailbox-root: test/
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+mkdir "$MH_TEST_DIR/panda"
+mkdir "$MH_TEST_DIR/panda/test"
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+cat > "$MH_TEST_DIR/panda/test/fold1" <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: 1603585238 1
+Status: RO
+X-Status:
+X-Keywords:
+X-UID: 1
+
+body
+
+EOF
+
+start_test 'inc'
+check_exit '-eq 0' inc +imap
+
+################################################################################
+start_test 'rmm over IMAP'
+
+check_exit '-eq 0' rmm -unlink +imap/fold1 1
+
+################################################################################
+start_test 'scan shows no messages'
+
+cat > "$expected" <<EOF
+scan: no messages match specification
+EOF
+scan -width 80 +imap/fold1 > "$actual" 2>&1 || :
+check "$actual" "$expected"
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+testname=${0#$srcdir/}
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+setup_test_without_data Mail
+
+rm -rf "$MH_TEST_DIR/panda"
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+start_fakeinetd env "HOME=$MH_TEST_DIR/panda" "$PANDA_IMAPD"
+imapd_pid=$fakeinetd_pid
+
+cat >> "$MH" <<EOF
+Unseen-Sequence: unseen
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-auth: PREAUTH
+imap-tls: don't protect my data i know what i'm doing
+imap-mailbox-root: test/
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+mkdir "$MH_TEST_DIR/panda"
+mkdir "$MH_TEST_DIR/panda/test"
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+# TODO duplicated with test-inc
+removed_message=5
+arith_val=1
+message_count=11
+cat > $MH_TEST_DIR/panda/test/fold <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: 1603585238 $message_count testseq
+Status: RO
+X-Status:
+X-Keywords:
+X-UID: $arith_val
+
+body
+
+EOF
+echo " $arith_val 10/24 me <<body >>" > "$expected.scan-all"
+
+while [ $arith_val -lt $message_count ]; do
+ arith_eval $arith_val + 1
+ printf ' %2d 10/24 me <<body >>\n' $arith_val >> "$expected.scan-all"
+ printf ' %2d U 10/24 me <<body >>\n' $arith_val >> "$expected.scan-unseen"
+ cat >> $MH_TEST_DIR/panda/test/fold <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: $arith_val
+
+body
+
+EOF
+ arith_eval $arith_val + 1
+ if [ $arith_val -ne $removed_message ]; then
+ printf ' %2d U 10/24 me <<body >>\n' $arith_val >> "$expected.scan-unseen"
+ printf ' %2d 10/24 me <<body >>\n' $arith_val >> "$expected.scan-all"
+ fi
+ cat >> $MH_TEST_DIR/panda/test/fold <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+Status: O
+X-Status:
+X-Keywords:
+X-UID: $arith_val
+
+body
+
+EOF
+done
+
+start_test 'inc then remove mesage 5'
+check_exit '-eq 0' inc +imap
+check_exit '-eq 0' rmm -unlink +imap/fold $removed_message
+
+################################################################################
+start_test 'scan all implicitly'
+
+run_prog scan +imap/fold -width 80 > "$actual"
+check "$expected.scan-all" "$actual" 'keep first'
+
+################################################################################
+start_test 'scan all explicitly'
+
+run_prog scan +imap/fold -width 80 all > "$actual"
+check "$expected.scan-all" "$actual" 'keep first'
+
+################################################################################
+start_test 'scan unseen'
+
+run_prog scan +imap/fold -form scan.unseen -width 80 unseen > "$actual"
+check "$expected.scan-unseen" "$actual"
+
+cat > "$expected" <<EOF
+nmh-picked: 2-4 6-11
+unseen: 2-4 6-11
+EOF
+check "$MH_TEST_DIR/Mail/imap/fold/.mh_sequences" "$expected" 'keep first'
+
+################################################################################
+start_test 'scan some messages'
+
+cat > "$expected" <<EOF
+ 2 U 10/24 me <<body >>
+ 3 U 10/24 me <<body >>
+ 4 U 10/24 me <<body >>
+ 6 U 10/24 me <<body >>
+ 8 U 10/24 me <<body >>
+ 9 U 10/24 me <<body >>
+ 10 U 10/24 me <<body >>
+EOF
+run_prog scan +imap/fold -form scan.unseen -width 80 8-9 9-10 2-6 > "$actual"
+check "$expected" "$actual"
+
+cat > "$expected" <<EOF
+nmh-picked: 2-4 6 8-10
+unseen: 2-4 6-11
+EOF
+check "$MH_TEST_DIR/Mail/imap/fold/.mh_sequences" "$expected" 'keep first'
+
+################################################################################
+start_test 'scan non-existent message'
+
+cat > "$expected" <<EOF
+scan: no messages match specification
+EOF
+
+run_prog scan +imap/fold -form scan.unseen -width 80 $removed_message > "$actual" 2>&1 || :
+check "$expected" "$actual"
+
+################################################################################
+start_test 'mark -del testseq unseen'
+
+check_exit '-eq 0' mark -add -seq testseq 6 8 10
+# TODO check output
+check_exit '-eq 0' scan testseq
+
+check_exit '-eq 0' mark -del -seq testseq unseen
+
+# TODO should be 'scan: bad message list testseq'
+cat > "$expected" <<EOF
+scan: no messages match specification
+EOF
+
+run_prog scan +imap/fold -form scan.unseen -width 80 testseq > "$actual" 2>&1 || :
+check "$expected" "$actual"
+
+################################################################################
+start_test 'show unseen twice'
+
+sed 's/^ 11 / 11+/' "$expected.scan-all" > "$expected"
+
+check_exit '-eq 0' show unseen unseen
+
+run_prog scan +imap/fold -width 80 > "$actual"
+check "$expected" "$actual"
+
+################################################################################
+start_test 'show 1-11'
+
+sed 's/^ 11 / 11+/' "$expected.scan-all" > "$expected"
+rm "$expected.scan-all"
+
+check_exit '-eq 0' show 1-11
+
+################################################################################
+start_test 'scan testseq'
+
+check_exit '-eq 0' mark -add -seq testseq 2-3 6-8 11
+cat > "$expected" <<EOF
+ 2 10/24 me <<body >>
+ 3 10/24 me <<body >>
+ 6 10/24 me <<body >>
+ 7 10/24 me <<body >>
+ 8 10/24 me <<body >>
+ 11+ 10/24 me <<body >>
+EOF
+run_prog scan +imap/fold -form scan.unseen -width 80 testseq > "$actual"
+check "$expected" "$actual"
+
+################################################################################
+start_test 'mark -del testseq 1'
+
+check_exit '-eq 0' mark -del -seq testseq 2
+
+cat > "$expected" <<EOF
+ 3 10/24 me <<body >>
+ 6 10/24 me <<body >>
+ 7 10/24 me <<body >>
+ 8 10/24 me <<body >>
+ 11+ 10/24 me <<body >>
+EOF
+run_prog scan +imap/fold -form scan.unseen -width 80 testseq > "$actual"
+check "$expected" "$actual"
+
+################################################################################
+start_test 'mark -del testseq all'
+
+check_exit '-eq 0' mark -del -seq testseq testseq
+
+################################################################################
+start_test 'scan testseq after mark -del'
+
+# TODO should be 'scan: bad message list testseq'
+cat > "$expected" <<EOF
+scan: no messages match specification
+EOF
+
+run_prog scan +imap/fold -form scan.unseen -width 80 testseq > "$actual" 2>&1 || :
+check "$expected" "$actual"
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+# common.sh sets this to something crazy; did i do that?!
+testname=${0#$srcdir/}
+
+setup_test_without_data Mail
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ rm -f "$imapd"
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+imapd="$MH_TEST_DIR/$$.imapd"
+start_fakeinetd "$imapd"
+imapd_pid=$fakeinetd_pid
+
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: PREAUTH
+imap-mailbox-root: test/
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* PREAUTH [CAPABILITY IMAP4REV1 UIDPLUS] authenticated\r
+* LIST (\NoInferiors \UnMarked) "/" test/fold\r
+1 OK LIST completed\r
+* 6 EXISTS\r
+* OK [UIDVALIDITY 1633920336] UID validity status\r
+* OK [UIDNEXT 11] Predicted next UID\r
+1 OK [READ-WRITE] SELECT for inc completed\r
+* 2 FETCH (UID 6)\r
+* 3 FETCH (UID 7)\r
+* 4 FETCH (UID 8)\r
+* 5 FETCH (UID 9)\r
+* 6 FETCH (UID 10)\r
+1 OK UID FETCH completed\r
+* 2 FETCH (UID 6 FLAGS (\Seen) BODY[] {9}\r
+from:me\r
+)\r
+f6 OK UID FETCH completed\r
+* 3 FETCH (UID 7 FLAGS (\Seen) BODY[] {9}\r
+from:me\r
+)\r
+f7 OK UID FETCH completed\r
+* 4 FETCH (UID 8 FLAGS (\Seen) BODY[] {9}\r
+from:me\r
+)\r
+f8 OK UID FETCH completed\r
+* 5 FETCH (UID 9 FLAGS (\Seen) BODY[] {9}\r
+from:me\r
+)\r
+f9 OK UID FETCH completed\r
+* 6 FETCH (UID 10 FLAGS (\Seen) BODY[] {9}\r
+from:me\r
+)\r
+f10 OK UID FETCH completed\r
+* SEARCH 9\r
+* SEARCH 7 6 10\r
+* SEARCH 8\r
+1 OK UID SEARCH completed\r
+close OK CLOSE for inc completed\r
+* 6 EXISTS\r
+* OK [UIDVALIDITY 1633920336] UID validity status\r
+* OK [UIDNEXT 11] Predicted next UID\r
+1 OK [READ-WRITE] SELECT for scan completed\r
+1 OK nothing for SEARCH NOT SEEN
+close OK CLOSE for scan completed\r
+EOF
+while read line; do :; done
+EOSCRIPT
+chmod 755 "$imapd"
+
+################################################################################
+start_test 'out of order SEARCH response - inc'
+
+check_exit '-eq 0' inc +imap
+
+check_exit '-eq 0' "$MH_OBJ_DIR/target/debug/mhimap" imap check imap/fold
+
+################################################################################
+start_test 'out of order SEARCH response - scan finds all 5 messages'
+
+cat > "$expected" <<EOF
+ 1 10/11*me
+ 2 10/11*me
+ 3 10/11*me
+ 4 10/11*me
+ 5 10/11*me
+EOF
+scan +imap/fold > "$actual"
+check "$actual" "$expected"
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+# common.sh sets this to something crazy; did i do that?!
+testname=${0#$srcdir/}
+
+setup_test_without_data Mail
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ rm -f "$imapd"
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+imapd="$MH_TEST_DIR/$$.imapd"
+start_fakeinetd "$imapd"
+imapd_pid=$fakeinetd_pid
+
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: PREAUTH
+imap-mailbox-root: test/
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+start_test 'unrecoverable error: server closes connection mid-LIST-response'
+
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* PREAUTH [CAPABILITY IMAP4REV1 UIDPLUS] authenticated\r
+* LIST (\NoInferiors \UnMarked) "/" test/fold\r
+EOF
+read line
+EOSCRIPT
+chmod 755 "$imapd"
+
+# This is similar to but NOT THE SAME as EOF tests in test-login; those are during authentication, these are after.
+
+cat > "$expected" <<EOF
+inc: failed to inc folder imap: Received EOF on network read
+EOF
+inc +imap > "$actual" 2>&1 || status=$?
+check "$actual" "$expected"
+(exit $status) && fail 'inc exited 0 on error'
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+testname=${0#$srcdir/}
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+rm -rf "$MH_TEST_DIR/panda"
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+
+ # TODO uh oops don't need two profiles for this one
+ for i in 1 2; do
+ set_mh_profile profile$i
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+ done
+
+ if [ -z "$MH_TEST_NOCLEANUP" ]; then
+ rm -rf "$MH_TEST_DIR/panda"
+ for i in 1 2; do
+ rm -rf "$MH_TEST_DIR/profile$i"
+ done
+ fi
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+
+mkdir "$MH_TEST_DIR/panda"
+mkdir "$MH_TEST_DIR/panda/test"
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+uidvalidity=1503585238
+cat > $MH_TEST_DIR/panda/test/fold <<EOF
+From nmh@example.com Sat Oct 24 19:20:38 2020 -0500
+Date:Sat, 24 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: $uidvalidity 1 testseq
+Status: RO
+X-Status:
+X-Keywords:
+X-UID: 1
+
+body
+
+EOF
+
+start_fakeinetd env "HOME=$MH_TEST_DIR/panda" "$PANDA_IMAPD"
+imapd_pid=$fakeinetd_pid
+
+start() {
+ rm -f "$MH_TEST_DIR/$profile/imap.d/socket"
+ status=0
+ LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw" "$MH_OBJ_DIR/target/debug/mhimap" imap start > "$actual" 2>&1 || status=$?
+}
+
+# setup profile 2 and 1, leaving 1 as active profile
+for i in 2 1; do
+ setup_test_without_data profile$i
+ trap imap_cleanup 0
+ cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/profile$i/imap.d/socket
+imap-db: $MH_TEST_DIR/profile$i/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: PREAUTH
+imap-mailbox-root: test/
+EOF
+ if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ fi
+ start
+done
+
+################################################################################
+start_test '1. inc from profile1'
+
+echo 'Incorporating new mail into imap/fold...' > "$expected.inc"
+echo >> "$expected.inc"
+printf ' 1 10/24 me <<body >>\n' >> "$expected.inc"
+
+inc +imap -form scan.unseen -width 80 > "$actual"
+check "$actual" "$expected.inc"
+
+################################################################################
+# 2. rebuild mailbox
+uidvalidity=1603585238
+cat > $MH_TEST_DIR/panda/test/fold <<EOF
+From nmh@example.com Sat Oct 25 19:20:38 2020 -0500
+Date:Sat, 25 Oct 2020 19:20:38 -0500
+from:me
+to:you
+X-IMAPbase: $uidvalidity 1 testseq
+Status: RO
+X-Status:
+X-Keywords:
+X-UID: 1
+
+body
+
+EOF
+
+################################################################################
+start_test '3. inc after rebuilt mailbox goes BOOM'
+
+# Kill the folder manager so inc will cause a re-SELECT.
+"$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+# TODO plumb this error out to inc
+
+check_exit '-eq 1' inc +imap
+if ! grep -q 'UIDVALIDITY changed' "$MH_TEST_DIR/profile$i/imap.d/stderr"; then
+ cat "$MH_TEST_DIR/profile$i/imap.d/stderr" >&2
+ fail "'UIDVALIDITY changed' not in $MH_TEST_DIR/profile$i/imap.d/stderr"
+fi
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+# TODO fold into test-login?
+
+if test -z "$MH_OBJ_DIR"; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "${srcdir}/test/oauth/common.sh"
+
+# common.sh sets this to something crazy; did i do that?!
+testname=${0#$srcdir/}
+
+setup_test_without_data Mail
+
+setup_service
+fake_creds <<EOF
+access-testuser: test-access
+refresh-testuser: test-refresh
+expire-testuser: 2000000000
+EOF
+
+imap_cleanup() {
+ #### Save exit status to use as status for this program.
+ status=$?
+ set +e
+
+ rm -f "$imapd"
+
+ [ -n "$imapd_pid" ] && kill $imapd_pid
+ "$MH_OBJ_DIR/target/debug/mhimap" imap shutdown
+
+ # TODO have cleanup allow additional cleanup registration?
+ # Or just allow it to accept status as argument.
+ (exit $status)
+ cleanup
+}
+trap imap_cleanup 0
+
+covdir="$MH_OBJ_DIR/covdata/${MH_COVERAGE_PROFILE:-default}/$testname"
+install -d "$covdir"
+export LLVM_PROFILE_FILE="$covdir/$profile-%m.profraw"
+
+imapd="$MH_TEST_DIR/$$.imapd"
+start_fakeinetd "$imapd"
+imapd_pid=$fakeinetd_pid
+
+cat >> "$MH" <<EOF
+Managed-Folders: imap
+imap-manager: $MH_OBJ_DIR/target/debug/imap-folder-manager
+imap-socket: $MH_TEST_DIR/Mail/imap.d/socket
+imap-db: $MH_TEST_DIR/Mail/imap.d
+imap-host: 127.0.0.1
+imap-port: $fakeinetd_port
+imap-tls: don't protect my data i know what i'm doing
+imap-auth: SASL
+imap-user: testuser
+imap-saslmech: xoauth2
+imap-authservice: test
+EOF
+if [ -n "$MH_TEST_IMAP_LOG_LEVEL" ]; then
+ echo "imap-log-level: $MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+ echo "imap-log-$MH_TEST_IMAP_LOG_LEVEL-to: $MH_TEST_DIR/$profile/imap.d/log.$MH_TEST_IMAP_LOG_LEVEL" >> "$MH"
+fi
+
+# TODO OAUTHBEARER? oooooh
+# TODO when imlementing capability put this after the greeting
+# * CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH2 AUTH=PLAIN AUTH=PLAIN-CLIENTTOKEN AUTH=OAUTHBEARER AUTH=XOAUTH
+# 1 OK
+cat > "$imapd" <<EOSCRIPT
+#!/bin/sh
+cat <<EOF
+* OK Gimap ready for requests\r
+* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 XYZZY SASL-IR AUTH=XOAUTH2 AUTH=PLAIN AUTH=PLAIN-CLIENTTOKEN AUTH=OAUTHBEARER AUTH=XOAUTH\r
+1 OK capability response\r
+* CAPABILITY IMAP4rev1 UNSELECT IDLE NAMESPACE QUOTA ID XLIST CHILDREN X-GM-EXT-1 UIDPLUS COMPRESS=DEFLATE ENABLE MOVE CONDSTORE ESEARCH UTF8=ACCEPT LIST-EXTENDED LIST-STATUS LITERAL- SPECIAL-USE APPENDLIMIT=35651584\r
+1 OK authenticate response\r
+1 OK LIST completed\r
+EOF
+while read line; do :; done
+EOSCRIPT
+chmod 755 "$imapd"
+
+# TODO lighter-weight command? Just trying to show auth worked.
+check_exit '-eq 0' inc +imap
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#include "h/mh.h"
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "h/folder_manager_protocol.h"
+#include "sbr/m_getfld.h"
+
+static int
+run_message_len(char const *const *const argv)
+{
+ // goto out to return this status and free these allocations
+ char *msg = NULL;
+ int status = 1;
+
+ errno = 0;
+ size_t const size = strtol(*argv, NULL, 10);
+ if (errno != 0) {
+ perror(*argv);
+ goto out;
+ }
+
+ msg = malloc(size);
+ if (read(0, msg, size) != (ssize_t)size) {
+ fprintf(stderr, "failed to read %ld bytes from stdin\n", size);
+ goto out;
+ }
+
+ status = mh_folder_manager_protocol_write_message(stdout, msg, size) ? 0 : 1;
+
+ out:
+ free(msg);
+ return status;
+}
+
+static int
+run_read_message(char const *const *argv)
+{
+ long size = 0, message_count = 0;
+ for (; *argv != NULL; argv++) {
+ if (strcmp(*argv, "-initial-vector-size") == 0) {
+ if (size > 0) {
+ fputs("1 protocol-tester -initial-vector-size N -message-count N\n", stderr);
+ return 1;
+ }
+ errno = 0;
+ size = strtol(*(++argv), NULL, 10);
+ if (errno != 0) {
+ perror(*argv);
+ return 1;
+ }
+ } else if (strcmp(*argv, "-message-count") == 0) {
+ if (message_count > 0) {
+ fputs("2 protocol-tester -initial-vector-size N -message-count N\n", stderr);
+ return 1;
+ }
+ errno = 0;
+ message_count = strtol(*(++argv), NULL, 10);
+ if (errno != 0) {
+ perror(*argv);
+ return 1;
+ }
+ } else {
+ fprintf(stderr, "what is this %s\n", *argv);
+ fputs("3 protocol-tester -initial-vector-size N -message-count N\n", stderr);
+ return 1;
+ }
+ }
+ if (size < 1 || message_count < 1) {
+ fputs("4 protocol-tester -initial-vector-size N -message-count N\n", stderr);
+ return 1;
+ }
+
+ mh_folder_manager_protocol_read_message_t state;
+ state.getfld_ctx = m_getfld_state_init(stdin);
+
+ svector_t message = NULL;
+ for (int i = 0; i < message_count; i++) {
+ free_svector_and_elements(&message);
+ message = svector_create(size);
+ if (!mh_folder_manager_protocol_read_message(message, &state)) {
+ printf("msg%d size=%zu mh_folder_manager_protocol_read_message failed\n", i, svector_size(message));
+ return 2;
+ }
+ printf("msg%d size=%zu\n", i, svector_size(message));
+ for (size_t j = 0; j < svector_size(message); j++) {
+ char *name = svector_at(message, j++);
+ char *value = svector_at(message, j);
+ printf("%s = %s\n", name, value);
+ }
+ }
+
+ m_getfld_state_destroy(&state.getfld_ctx);
+
+ return 0;
+}
+
+int
+main(int const argc, char const *const *const argv)
+{
+ if (argc < 2) {
+ fputs("too few arguments\n", stderr);
+ return 1;
+ }
+ if (strcmp(argv[1], "message-len") == 0) {
+ return run_message_len(argv + 2);
+ }
+ if (strcmp(argv[1], "read-message") == 0) {
+ return run_read_message(argv + 2);
+ }
+ fprintf(stderr, "unknown imap/protocol-tester command %s\n", argv[1]);
+ return 1;
+}
--- /dev/null
+#!/bin/sh
+
+if [ -z "$MH_OBJ_DIR" ]; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+setup_test_without_data
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+################################################################################
+start_test 'ask protocol-tester to read too much'
+
+check_exit '-eq 1' "$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 1 < /dev/null
+
+################################################################################
+start_test 'len-only empty message'
+
+echo 'len:6' > "$expected"
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 0 < /dev/null > "$actual"
+check "$expected" "$actual"
+
+################################################################################
+start_test '7 characters'
+
+cat > "$expected" <<EOF
+len:7
+
+EOF
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 1 > "$actual" <<EOF
+
+EOF
+check "$expected" "$actual"
+
+################################################################################
+start_test '8 characters'
+
+cat > "$expected" <<EOF
+len:8
+.
+EOF
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 2 > "$actual" <<EOF
+.
+EOF
+check "$expected" "$actual"
+
+################################################################################
+start_test '9 characters'
+
+cat > "$expected" <<EOF
+len:9
+..
+EOF
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 3 > "$actual" <<EOF
+..
+EOF
+check "$expected" "$actual"
+
+################################################################################
+start_test '7 characters'
+
+cat > "$expected" <<EOF
+len:11
+...
+EOF
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 4 > "$actual" <<EOF
+...
+EOF
+check "$expected" "$actual"
+
+################################################################################
+start_test '99 characters'
+
+cat > "$expected" <<EOF
+len:98
+..........................................................................................
+EOF
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 91 > "$actual" <<EOF
+..........................................................................................
+EOF
+check "$expected" "$actual"
+
+################################################################################
+start_test '100 characters'
+
+cat > "$expected" <<EOF
+len:99
+...........................................................................................
+EOF
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 92 > "$actual" <<EOF
+...........................................................................................
+EOF
+check "$expected" "$actual"
+
+################################################################################
+start_test '101 characters'
+
+cat > "$expected" <<EOF
+len:101
+............................................................................................
+EOF
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 93 > "$actual" <<EOF
+............................................................................................
+EOF
+check "$expected" "$actual"
+
+################################################################################
+start_test '102 characters'
+
+cat > "$expected" <<EOF
+len:102
+.............................................................................................
+EOF
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 94 > "$actual" <<EOF
+.............................................................................................
+EOF
+check "$expected" "$actual"
+
+################################################################################
+start_test '103 characters'
+
+cat > "$expected" <<EOF
+len:103
+..............................................................................................
+EOF
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 95 > "$actual" <<EOF
+..............................................................................................
+EOF
+check "$expected" "$actual"
+
+################################################################################
+start_test '104 characters'
+
+cat > "$expected" <<EOF
+len:104
+...............................................................................................
+EOF
+
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" message-len 96 > "$actual" <<EOF
+...............................................................................................
+EOF
+check "$expected" "$actual"
+
+# TODO this is just a bunch of captured messages; don't need to keep all these
+
+cat > $expected <<EOF
+len:26
+op:inc
+folder:imap
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 19 > $actual <<EOF
+op:inc
+folder:imap
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:42
+op:pick
+folder:imap/fold2
+msgs:all
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 35 > $actual <<EOF
+op:pick
+folder:imap/fold2
+msgs:all
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:55
+op:make-folder
+folder:imap/imap356135249/drafts
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 48 > $actual <<EOF
+op:make-folder
+folder:imap/imap356135249/drafts
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:53
+op:make-folder
+folder:imap/imap356135249/sent
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 46 > $actual <<EOF
+op:make-folder
+folder:imap/imap356135249/sent
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:40
+op:inc
+folder:imap/imap356135249
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 33 > $actual <<EOF
+op:inc
+folder:imap/imap356135249
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:57
+op:pick
+folder:imap/imap356135249/drafts
+msgs:all
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 50 > $actual <<EOF
+op:pick
+folder:imap/imap356135249/drafts
+msgs:all
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:55
+op:show
+folder:imap/imap356135249/drafts
+msgs:1
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 48 > $actual <<EOF
+op:show
+folder:imap/imap356135249/drafts
+msgs:1
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:55
+op:pick
+folder:imap/imap356135249/sent
+msgs:all
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 48 > $actual <<EOF
+op:pick
+folder:imap/imap356135249/sent
+msgs:all
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:40
+op:inc
+folder:imap/imap356135249
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 33 > $actual <<EOF
+op:inc
+folder:imap/imap356135249
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:53
+op:show
+folder:imap/imap356135249/sent
+msgs:1
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 46 > $actual <<EOF
+op:show
+folder:imap/imap356135249/sent
+msgs:1
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:55
+op:pick
+folder:imap/imap356135249/sent
+msgs:all
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 48 > $actual <<EOF
+op:pick
+folder:imap/imap356135249/sent
+msgs:all
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:55
+op:pick
+folder:imap/imap356135249/sent
+msgs:all
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 48 > $actual <<EOF
+op:pick
+folder:imap/imap356135249/sent
+msgs:all
+EOF
+check $expected $actual
+
+cat > $expected <<EOF
+len:47
+op:rmf
+folder:imap/imap356135249/drafts
+EOF
+$MH_OBJ_DIR/test/managed-folders/protocol-tester message-len 40 > $actual <<EOF
+op:rmf
+folder:imap/imap356135249/drafts
+EOF
+check $expected $actual
+
+finish_test
+exit ${failed:-0}
--- /dev/null
+#!/bin/sh
+
+if [ -z "$MH_OBJ_DIR" ]; then
+ srcdir=`dirname "$0"`/../..
+ MH_OBJ_DIR=`cd "$srcdir" && pwd`; export MH_OBJ_DIR
+fi
+
+. "$MH_OBJ_DIR/test/common.sh"
+
+setup_test_without_data
+
+expected="$MH_TEST_DIR/$$.expected"
+actual="$MH_TEST_DIR/$$.actual"
+
+start_test 'one 32-component message with initial vector size of 1'
+cat > "$expected" <<EOF
+msg0 size=66
+len = 471
+comp1 = value1
+comp2 = value2
+comp3 = value3
+comp4 = value4
+comp5 = value5
+comp6 = value6
+comp7 = value7
+comp8 = value8
+comp9 = value9
+comp10 = value10
+comp11 = value11
+comp12 = value12
+comp13 = value13
+comp14 = value14
+comp15 = value15
+comp16 = value16
+comp17 = value17
+comp18 = value18
+comp19 = value19
+comp20 = value20
+comp21 = value21
+comp22 = value22
+comp23 = value23
+comp24 = value24
+comp25 = value25
+comp26 = value26
+comp27 = value27
+comp28 = value28
+comp29 = value29
+comp30 = value30
+comp31 = value31
+comp32 = value32
+EOF
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" read-message -initial-vector-size 1 -message-count 1 > "$actual" <<EOF
+len:471
+comp1:value1
+comp2:value2
+comp3:value3
+comp4:value4
+comp5:value5
+comp6:value6
+comp7:value7
+comp8:value8
+comp9:value9
+comp10:value10
+comp11:value11
+comp12:value12
+comp13:value13
+comp14:value14
+comp15:value15
+comp16:value16
+comp17:value17
+comp18:value18
+comp19:value19
+comp20:value20
+comp21:value21
+comp22:value22
+comp23:value23
+comp24:value24
+comp25:value25
+comp26:value26
+comp27:value27
+comp28:value28
+comp29:value29
+comp30:value30
+comp31:value31
+comp32:value32
+EOF
+check "$expected" "$actual"
+
+start_test 'two messages'
+cat > "$expected" <<EOF
+msg0 size=4
+len = 19
+cmp1 = value
+msg1 size=4
+len = 19
+cmp2 = value
+EOF
+"$MH_OBJ_DIR/test/managed-folders/protocol-tester" read-message -initial-vector-size 16 -message-count 2 > "$actual" <<EOF
+len:19
+cmp1:value
+len:19
+cmp2:value
+EOF
+check "$expected" "$actual"
+
+# TODO This is a limitation of using m_getfld2 to parse messages.
+# Not sure we can fix it as long as we're using m_getfld2.
+# But reuse of m_getfld2 is the entire reason for the protocol taking this particular form.
+# The protocol is entirely internal so it doesn't really matter...
+start_test 'two messages with no newline between them does not work'
+cat > "$expected" <<EOF
+we read 24 instead of 18
+msg0 size=4 mh_folder_manager_protocol_read_message failed
+EOF
+("$MH_OBJ_DIR/test/managed-folders/protocol-tester" read-message -initial-vector-size 16 -message-count 2 > "$actual" 2>&1 || :) <<EOF
+len:18
+cmp1:valuelen:19
+cmp2:value
+EOF
+check "$expected" "$actual"
+
+finish_test
+exit $failed
. "${MH_OBJ_DIR}/test/common.sh"
-setup_test
+# TODO move to oauth test scripts and make them call setup_service
+#setup_test
if [ "${OAUTH_SUPPORT}" -eq 0 ]; then
test_skip 'no oauth support'
arith_eval ${pop_port} + 1
smtp_port=${arith_val}
-cat >> ${MH} <<EOF
+setup_service() {
+ cat >> ${MH} <<EOF
oauth-test-scope: test-scope
oauth-test-client_id: test-id
oauth-test-client_secret: test-secret
oauth-test-token_endpoint: http://127.0.0.1:${http_port}/oauth/token
oauth-test-redirect_uri: urn:ietf:wg:oauth:2.0:oob
EOF
+}
setup_pop() {
pop_message=${MHTMPDIR}/testmessage
/*
* If there is a pid file already around, kill the previously running
- * fakesmtp process. Hopefully this will reduce the race conditions
+ * server process. Hopefully this will reduce the race conditions
* that crop up when running the test suite.
*/
{
int i, vecp = 0;
bool inverted, list, noalias;
- char *cp, **ap, **argp, buf[BUFSIZ];
+ char *cp, **argp, buf[BUFSIZ];
/* Really only need to allocate for argc-1, but must allocate at least 1,
so go ahead and allocate for argc char pointers. */
char **vec = mh_xmalloc (argc * sizeof (char *)), **arguments;
if ((cp = context_find ("Aliasfile"))) {
char *dp = NULL;
- for (ap = brkstring(dp = mh_xstrdup(cp), " ", "\n"); ap && *ap; ap++)
+ for (char *const *ap = brkstring(dp = mh_xstrdup(cp), " ", "\n"); ap && *ap; ap++)
if ((i = alias (*ap)) != AK_OK)
die("aliasing error in %s - %s", *ap, akerror (i));
free(dp);
#include "sbr/showfile.h"
#include "sbr/smatch.h"
#include "sbr/refile.h"
+#include "sbr/concat.h"
#include "sbr/cpydata.h"
#include "sbr/m_draft.h"
#include "sbr/m_convert.h"
#include "h/utils.h"
#include "h/fmt_scan.h"
#include "h/done.h"
+#include "h/managed_folders.h"
#include "sbr/m_maildir.h"
#include <fcntl.h>
bool build = false;
int i, in = NOTOK, isdf = 0, out, dat[5], format_len = 0;
int outputlinelen = OUTPUTLINELEN;
- char *cp, *cwd, *maildir, *dfolder = NULL;
+ char *cp, *cwd, *maildir = NULL, *dfolder = NULL;
char *ed = NULL, *file = NULL, *form = NULL;
char *folder = NULL, *msg = NULL, buf[BUFSIZ];
char *to = NULL, *from = NULL, *cc = NULL, *fcc = NULL, *dp;
char *subject = NULL;
- char drft[BUFSIZ], **argp, **arguments;
+ char *drft;
+ int drft_msgnum;
+ char draft_fn_buf[BUFSIZ], **argp, **arguments;
struct msgs *mp = NULL;
struct format *fmt;
struct stat st;
+ mh_managed_folders *mmfs = NULL;
+ mh_managed_folder *mmf;
+ mh_folder_manager_client *mfmc = NULL;
if (nmh_init(argv[0], true, true)) { return 1; }
/*
* Check if we are using a draft folder
- * and have specified a message in it.
*/
- if ((dfolder || context_find ("Draft-Folder")) && !folder && msg && !file) {
- file = msg;
- msg = NULL;
+ if (dfolder == NULL ) {
+ // TODO m_draft redoes this check; stop it.
+ dfolder = context_find("Draft-Folder");
}
+ bool draft_folder_is_managed = false;
+ {
+ if (dfolder != NULL) {
+ if (!mh_managed_folders_open(&mmfs)) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ mmf = mh_managed_folder_lookup(mmfs, dfolder, strlen(dfolder));
+ if (mmf == NULL) {
+ /* and have specified a message in it. */
+ if (!folder && msg && !file) {
+ file = msg;
+ msg = NULL;
+ }
+ } else {
+ draft_folder_is_managed = true;
+ if (!nwhat || build || folder || msg || file) {
+ die("only simple comp supports managed folder");
+ }
+ maildir = m_maildir (dfolder);
+ if (chdir (maildir) == NOTOK)
+ adios (maildir, "unable to change directory to");
+ if (!mh_folder_manager_connect(&mfmc, mmf)) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ drft_msgnum = mh_folder_manager_create_draft(mfmc, dfolder);
+ if (drft_msgnum == 0) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ drft = m_name(drft_msgnum);
+ }
+ }
+ }
+
if (form && (folder || msg))
die("can't mix forms and folders/msgs");
}
}
+ if (!draft_folder_is_managed) {
try_it_again:
- strncpy (drft, build ? m_maildir ("draft")
+ strncpy (draft_fn_buf, build ? m_maildir ("draft")
: m_draft (dfolder, file, use, &isdf), sizeof(drft) - 1);
+ drft = draft_fn_buf;
+ }
/*
* Check if we have an existing draft
adios (drft, "unable to stat");
printf ("Draft \"%s\" exists (%ld bytes).", drft, (long) st.st_size);
for (i = LISTDSW; i != YESW;) {
- if (!(argp = read_switch_multiword ("\nDisposition? ",
- isdf ? aqrunl : aqrul)))
+ char *const *words = read_switch_multiword ("\nDisposition? ", isdf ? aqrunl : aqrul);
+ if (words == NULL)
done (1);
- switch (i = smatch (*argp, isdf ? aqrunl : aqrul)) {
+ switch (i = smatch (*words, isdf ? aqrunl : aqrul)) {
case NOSW:
done (0);
case NEWSW:
use++;
goto edit_it;
case LISTDSW:
- showfile (++argp, drft);
+ showfile (++words, drft);
break;
case REFILSW:
- if (refile (++argp, drft) == 0)
+ if (refile (++words, drft) == 0)
i = YESW;
break;
default:
edit_it:
context_save (); /* save the context file */
- if (nwhat)
+ if (nwhat) {
+ if (draft_folder_is_managed) {
+ if (!mh_folder_manager_connect(&mfmc, mmf)) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ if (!mh_folder_manager_update_draft(
+ mfmc,
+ dfolder,
+ drft_msgnum)) {
+ die("in trouble updating draft %s", mh_managed_folders_get_err_string(mmfs));
+ }
+ }
done (0);
+ }
+
what_now (ed, nedit, use, drft, NULL, 0, NULL, NULL, 0, cwd, 0);
done (1);
return 1;
try_it_again:
strncpy (drft, m_draft (dfolder, dmsg, NOUSE, &isdf), sizeof(drft) - 1);
+ // TODO(epg): Duplicated from comp.
/* Check if draft already exists */
if (stat (drft, &st) != NOTOK) {
printf ("Draft \"%s\" exists (%ld bytes).", drft, (long) st.st_size);
for (i = LISTDSW; i != YESW;) {
- if (!(argp = read_switch_multiword ("\nDisposition? ",
- isdf ? aqrnl : aqrl)))
+ char *const *const words = read_switch_multiword ("\nDisposition? ", isdf ? aqrnl : aqrl);
+ if (words == NULL)
done (1);
switch (i = smatch (*argp, isdf ? aqrnl : aqrl)) {
case NOSW:
* all the sequences defined there.
*/
if (svector_size (sequencesToDo) == 0) {
+ // TODO(epg): How many times is this duplicated?
if ((cp = context_find(usequence)) && *cp) {
- char **ap, *dp;
-
- dp = mh_xstrdup(cp);
- ap = brkstring (dp, " ", "\n");
+ char *const dp = mh_xstrdup(cp);
+ char *const *ap = brkstring (dp, " ", "\n");
for (; ap && *ap; ap++)
svector_push_back (sequencesToDo, *ap);
} else {
if (dat[4] == -1) {
if ((cp = context_find(usequence)) && *cp) {
- char **ap, *dp;
-
- dp = mh_xstrdup(cp);
- ap = brkstring(dp, " ", "\n");
+ char *const dp = mh_xstrdup(cp);
+ char *const *ap = brkstring(dp, " ", "\n");
for (i = 0; ap && *ap; i++, ap++)
ivector_push_back (seqnum, seq_getnum(mp, *ap));
*/
#include "h/mh.h"
+
+#include <string.h>
+
+#include "h/managed_folders.h"
#include "sbr/getarguments.h"
#include "sbr/concat.h"
#include "sbr/seq_setprev.h"
bool pushsw = false;
bool popsw = false;
char *cp, *dp, *msg = NULL, *argfolder = NULL;
- char **ap, **argp, buf[BUFSIZ], **arguments;
+ char *const *ap, **argp, buf[BUFSIZ], **arguments;
if (nmh_init(argv[0], true, true)) { return 1; }
crawl_folders (folder, get_folder_info_callback, NULL);
}
} else {
+ mh_managed_folders *mmfs;
+ if (!mh_managed_folders_open(&mmfs)) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+
strncpy (folder, argfolder ? argfolder : getfolder (1),
sizeof(folder) - 1);
* Check if folder exists. If not, then see if
* we should create it, or just exit.
*/
- create_folder (m_maildir (folder), fcreat, nonexistent_folder);
+
+ mh_folder_manager_client *mfmc = NULL;
+ mh_managed_folder *const mmf = mh_managed_folder_lookup(mmfs, folder, strlen(folder));
+ if (mmf == NULL || !fcreat) {
+ create_folder (m_maildir (folder), fcreat, nonexistent_folder);
+ } else {
+ if (!mh_folder_manager_connect(&mfmc, mmf)) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ if (!mh_folder_manager_make_folder(mfmc, folder)) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ }
if (get_folder_info (folder, msg) && argfolder) {
/* update current folder */
strncpy (drft, buildsw ? m_maildir ("draft")
: m_draft (dfolder, NULL, NOUSE, &isdf), sizeof(drft) - 1);
+ // TODO(epg): Duplicated from comp.
/* Check if a draft already exists */
if (!buildsw && stat (drft, &st) != NOTOK) {
printf ("Draft \"%s\" exists (%ld bytes).", drft, (long) st.st_size);
for (i = LISTDSW; i != YESW;) {
- if (!(argp = read_switch_multiword ("\nDisposition? ",
- isdf ? aqrnl : aqrl)))
+ char *const *const words = read_switch_multiword ("\nDisposition? ", isdf ? aqrnl : aqrl);
+ if (words == NULL)
done (1);
switch (i = smatch (*argp, isdf ? aqrnl : aqrl)) {
case NOSW:
parse_capability(const char *cap, unsigned int len)
{
char *str = mh_xmalloc(len + 1);
- char **caplist;
int i;
trunccpy(str, cap, len + 1);
- caplist = brkstring(str, " ", NULL);
+ char *const *const caplist = brkstring(str, " ", NULL);
if (imap_capabilities) {
svector_free(imap_capabilities);
#endif
#include "h/mh.h"
+
+#include <stdio.h>
+
#include "sbr/fmt_new.h"
#include "sbr/dtime.h"
#include "scansbr.h"
#include "h/dropsbr.h"
#include "popsbr.h"
#include "h/fmt_scan.h"
+#include "h/managed_folders.h"
#include "h/signals.h"
#include "h/tws.h"
#include "h/mts.h"
/*
* flags for the mail source
*/
-#define INC_FILE 0
-#define INC_POP 1
+typedef enum IncType {
+ INC_FILE,
+ INC_POP,
+ INC_MANAGED_FOLDER,
+} IncType;
static struct Maildir_entry *Maildir;
static int num_maildir_entries;
int
main (int argc, char **argv)
{
- static int inc_type;
+ IncType inc_type;
bool chgflag;
int trnflag = 1;
bool noisy;
int width = -1;
int hghnum = 0, msgnum = 0;
- FILE *pf = NULL;
bool sasl, noverify;
int tls = 0;
- int incerr = 0; /* <0 if inc hits an error which means it should not truncate mailspool */
- char *cp, *maildir = NULL, *folder = NULL;
+ char *cp, *folder = NULL;
char *format = NULL, *form = NULL;
char *host = NULL, *port = NULL, *user = NULL, *proxy = NULL;
char *audfile = NULL, *from = NULL, *saslmech = NULL, *auth_svc = NULL;
- char buf[BUFSIZ], **argp, *nfs, **arguments;
- struct msgs *mp = NULL;
- struct stat st, s1;
- FILE *aud = NULL;
- char b[PATH_MAX + 1];
- char *maildir_copy = NULL; /* copy of mail directory because the static gets overwritten */
- charstring_t scanl = NULL;
+ char buf[BUFSIZ], **argp, **arguments;
+ struct stat s1;
+ mh_managed_folders *mmfs;
+ mh_folder_manager_client *mfmc;
+ mh_folder_manager_inc_context *mfinc;
int nmsgs, nbytes;
char *MAILHOST_env_variable;
/* guarantee dropping group privileges; we might not have done so earlier */
DROPGROUPPRIVS();
- /* Source of mail; -from overrides any -host. */
- inc_type = host && !from ? INC_POP : INC_FILE;
+ if (!mh_managed_folders_open(&mmfs)) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ mh_managed_folder *const mmf = mh_managed_folder_lookup(mmfs, folder, strlen(folder));
+ if (mmf == NULL) {
+ /* Source of mail; -from overrides any -host. */
+ inc_type = host && !from ? INC_POP : INC_FILE;
+ } else {
+ inc_type = INC_MANAGED_FOLDER;
+ if (!mh_folder_manager_connect(&mfmc, mmf)) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ }
if (inc_type == INC_POP) {
/* Mail from a POP server. */
if (!context_find ("path"))
free (path ("./", TFOLDER));
if (!folder)
- folder = getfolder (0);
- maildir = m_maildir (folder);
- maildir_copy = mh_xstrdup(maildir);
+ folder = mh_xstrdup (getfolder (0));
+
+ // TODO the whole inc loop needs work; this is the least of the problems
+ int hack_loop_count = 0;
+ if (inc_type == INC_MANAGED_FOLDER) {
+ mfinc = mh_folder_manager_inc(mfmc, folder);
+ if (mfinc == NULL) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ }
+ /* We loop for INC_MANAGED_FOLDER only. */
+ for (;;) {
+ FILE *pf = NULL;
+ int incerr = 0; /* <0 if inc hits an error which means it should not truncate mailspool */
+ char *maildir = m_maildir (folder);
+ // TODO do we really need both?
+ /* copy of mail directory because the static gets overwritten */
+ char *maildir_copy = mh_xstrdup(maildir);
+ char *nfs;
+ struct msgs *mp = NULL;
+ FILE *aud = NULL;
+ char b[PATH_MAX + 1];
+ charstring_t scanl = NULL;
+
+
+ if (inc_type != INC_MANAGED_FOLDER) {
if (!folder_exists(maildir)) {
/* If the folder doesn't exist, and we're given the -silent flag,
* just fail.
/* read folder and create message structure */
if (!(mp = folder_read (folder, 0)))
die("unable to read folder %s", folder);
+ } /* inc_type != INC_MANAGED_FOLDER */
if (inc_type == INC_FILE && Maildir == NULL) {
/* Mail from a spool file. */
if (audfile) {
int i;
+ struct stat st;
if ((i = stat (audfile, &st)) == NOTOK)
inform("Creating Receive-Audit: %s", audfile);
if ((aud = fopen (audfile, "a")) == NULL)
/* Get new format string */
nfs = new_fs (form, format, FORMAT);
- if (noisy) {
+ if (inc_type != INC_MANAGED_FOLDER && noisy) {
printf ("Incorporating new mail into %s...\n\n", folder);
fflush (stdout);
}
if (pop_quit () == NOTOK)
die("%s", response);
- } else if (inc_type == INC_FILE && Maildir == NULL) {
+ /* Only loop for INC_MANAGED_FOLDER. */
+ break;
+ } else if (inc_type == INC_FILE) {
+ if (Maildir == NULL) {
/* Mail from a spool file. */
scan_detect_mbox_style (in); /* the MAGIC invocation... */
charstring_free (scanl);
scanl = NULL;
}
+ /* Only loop for INC_MANAGED_FOLDER. */
+ break;
+ } else {
+ const mh_managed_folder_inc_page *const page = mh_folder_manager_inc_next(mfinc);
+ if (page == NULL) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ if (page->done) {
+ break;
+ }
+
+ // TODO this is the problem. I wrote this for this case:
+ // inc +imap
+ // Incorporating new mail into imap/child1...
+ // Incorporating new mail into imap/child2...
+ // not for this case:
+ // inc +imap/child1
+ // Incorporating new mail into imap/child1...
+ // Because here I thought I was comparing 'imap' to 'imap/child1' on
+ // the first iteration, but in this case we are comparing
+ // 'imap/child1' to 'imap/child1' on the first iteration! I think
+ // the mistake was in reusing the 'folder' variable
+ if (++hack_loop_count == 1 || strcmp(page->folder, folder) != 0) {
+ free (folder);
+ folder = mh_xstrdup (page->folder);
+ printf ("Incorporating new mail into %s...\n\n", folder);
+ fflush (stdout);
+ maildir = m_maildir(folder);
+ free(maildir_copy);
+ maildir_copy = mh_xstrdup(maildir);
+ if (chdir (maildir) == NOTOK) {
+ adios(maildir, "unable to change directory to");
+ }
+ }
+
+ {
+ long TODO_FILE_SIZE = 32;
+
+ msgnum = page->msg;
+
+ cp = mh_xstrdup(m_name(msgnum));
+ if ((pf = fopen (cp, "r")) == NULL)
+ adios (cp, "unable to scan");
+ switch (incerr = scan (pf, msgnum, 0, nfs, width, 0,
+ page->unseen, NULL, TODO_FILE_SIZE, noisy, &scanl)) {
+ case SCNEOF:
+ printf ("%*d empty\n", DMAXFOLDER, msgnum);
+ break;
+
+ case SCNFAT:
+ trnflag = 0;
+ noisy = true;
+ /* advise (cp, "unable to read"); already advised */
+ break;
+
+ case SCNERR:
+ case SCNNUM:
+ break;
+
+ case SCNMSG:
+ default:
+ if (aud)
+ fputs (charstring_buffer (scanl), aud);
+ if (noisy)
+ fflush (stdout);
+ break;
+ }
+
+ if (scanl)
+ charstring_clear (scanl);
+
+ if (ferror(pf) || fclose (pf)) {
+ perror("TODO error during scan... do we really care?");
+ }
+ free (cp);
+
+ scan_finished();
+ }
+
+ charstring_free (scanl);
+ scanl = NULL;
+ }
scan_finished ();
+ if (inc_type != INC_MANAGED_FOLDER) {
if (incerr < 0) { /* error */
if (locked) {
GETGROUPPRIVS(); /* Be sure we can unlock mail file */
}
die("failed");
}
+ }
if (aud)
fclose (aud);
if (noisy)
fflush (stdout);
+ } /* loop for INC_MANAGED_FOLDER only */
+
if (inc_type == INC_FILE && Maildir == NULL) {
/* Mail from a spool file; truncate it. */
if (trnflag) {
+ struct stat st;
if (stat (newmail, &st) != NOTOK && s1.st_mtime != st.st_mtime)
inform("new messages have arrived!\007");
else {
}
}
+ if (inc_type != INC_MANAGED_FOLDER) {
if (msgnum == hghnum) {
inform("no messages incorporated, continuing...");
} else {
seq_save(mp2); /* Save the sequence file */
folder_free(mp2);
}
+ }
skip:
if (inc_type == INC_FILE && Maildir == NULL) {
*/
#include "h/mh.h"
+#include "h/managed_folders.h"
#include "sbr/getarguments.h"
#include "sbr/seq_save.h"
#include "sbr/smatch.h"
app_msgarg(&msgs, listsw ? "all" :"cur");
if (!folder)
folder = getfolder (1);
+
+ {
+ mh_managed_folders *mmfs;
+ if (!mh_managed_folders_open(&mmfs))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ mh_managed_folder *const mmf = mh_managed_folder_lookup(mmfs, folder, strlen(folder));
+ if (mmf != NULL) {
+ if (listsw || publicsw > 0 || zerosw)
+ die("unsupported switches for mark on managed folder");
+ mh_folder_manager_client *mfmc;
+ if (!mh_folder_manager_connect(&mfmc, mmf))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ if (addsw) {
+ if (!mh_folder_manager_mark_add(mfmc, folder, msgs, seqs))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ if (deletesw) {
+ if (!mh_folder_manager_mark_del(mfmc, folder, msgs, seqs))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ if (!mh_folder_manager_close(mfmc))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ done (0);
+ return 1;
+ }
+ }
+
maildir = m_maildir (folder);
if (chdir (maildir) == NOTOK)
user_content (FILE *in, char *buf, CT *ctp, const char *infilename)
{
int extrnal, vrsn;
- char *cp, **ap;
+ char *cp, *const *ap;
char buffer[NMH_BUFSIZ];
struct multipart *m;
struct part **pp;
#include <string.h>
#include "h/mh.h"
-#include "sbr/getarguments.h"
-#include "sbr/concat.h"
-#include "sbr/smatch.h"
-#include "sbr/ambigsw.h"
-#include "sbr/print_version.h"
-#include "sbr/print_help.h"
-#include "sbr/error.h"
+
#include "h/done.h"
-#include "h/utils.h"
#include "h/oauth.h"
+#include "h/utils.h"
+#include "sbr/ambigsw.h"
+#include "sbr/base64.h"
+#include "sbr/concat.h"
+#include "sbr/error.h"
+#include "sbr/getarguments.h"
#include "sbr/lock_file.h"
+#include "sbr/print_help.h"
+#include "sbr/print_version.h"
+#include "sbr/smatch.h"
#define MHLOGIN_SWITCHES \
- X("user username", 0, USERSW) \
- X("saslmech", 0, SASLMECHSW) \
X("authservice", 0, AUTHSERVICESW) \
X("browser", 0, BROWSERSW) \
- X("snoop", 0, SNOOPSW) \
X("help", 0, HELPSW) \
+ X("print-client-response", 0, PRINT_CLIENT_RES_SW) \
+ X("saslmech", 0, SASLMECHSW) \
+ X("snoop", 0, SNOOPSW) \
+ X("user username", 0, USERSW) \
X("version", 0, VERSIONSW) \
#define X(sw, minchars, id) id,
return 0;
}
+
+static int
+do_print_client_response(const char *svc, const char *user, const int snoop)
+{
+ unsigned char *oauth_res, *b64data;
+ size_t oauth_res_len;
+ mh_oauth_do_xoauth(user, svc, &oauth_res, &oauth_res_len, snoop ? stderr : NULL);
+ fwrite(oauth_res, 1, oauth_res_len, stdout);
+ b64data = mh_xmalloc(BASE64SIZE(oauth_res_len));
+ writeBase64raw(oauth_res, oauth_res_len, b64data);
+ printf("\n%s\n", b64data);
+ return 0;
+}
#endif
int
main(int argc, char **argv)
{
+ bool print_client_response = false;
char *cp, **argp, **arguments;
const char *user = NULL, *saslmech = NULL, *svc = NULL, *browser = NULL;
int snoop = 0;
die("missing argument to %s", argp[-2]);
continue;
+ case PRINT_CLIENT_RES_SW:
+ print_client_response = true;
+ continue;
case SNOOPSW:
snoop++;
continue;
free(arguments);
#ifdef OAUTH_SUPPORT
+ if (print_client_response)
+ return do_print_client_response(svc, user, snoop);
+
return do_login(svc, user, browser, snoop);
#else
NMH_UNUSED(svc);
* it to the end of the current "ignores" list.
*/
if (!strcasecmp (name, "ignores")) {
- char **tmparray, **p;
int n = 0;
/* split the fields */
- tmparray = brkstring (mh_xstrdup(++parptr), ",", NULL);
+ char *const *const tmparray = brkstring (mh_xstrdup(++parptr), ",", NULL);
/* count number of fields split */
- p = tmparray;
+ char *const *p = tmparray;
while (*p++)
n++;
{ "no", 0, NMH_NO },
{ "rename", 0, NMH_RENAME },
{ NULL, 0, 0 } };
- char **ans;
+ char *const *ans;
if (isatty (fileno (stdin))) {
char *prompt =
{
int total = 0;
int j, k;
- char *cp, **ap;
field = getcpy(field);
/* copied from seq_read.c:seq_init */
- for (ap = brkstring (field, " ", "\n"); *ap; ap++) {
- if ((cp = strchr(*ap, '-')))
+ for (char *const *ap = brkstring (field, " ", "\n"); *ap; ap++) {
+ char *cp = strchr(*ap, '-');
+ if (cp != NULL)
*cp++ = '\0';
if ((j = m_atoi (*ap)) > 0) {
k = cp ? m_atoi (cp) : j;
int
main(int argc, char **argv)
{
- char **ap, *cp, **argp, **arguments;
+ char *cp, **argp, **arguments;
char help[BUFSIZ];
char *folders = NULL;
svector_t sequences = svector_create (0);
if (unseen == NULL || unseen[0] == '\0') {
die("must specify sequences or set %s", usequence);
}
- for (ap = brkstring(unseen, " ", "\n"); *ap; ap++) {
+ for (char *const *ap = brkstring(unseen, " ", "\n"); *ap; ap++) {
svector_push_back (sequences, *ap);
++i;
}
#include "sbr/arglist.h"
#include "sbr/error.h"
#include "h/done.h"
+#include "h/managed_folders.h"
#include "h/utils.h"
#include "sbr/m_maildir.h"
#include "sbr/m_mktemp.h"
int
main (int argc, char **argv)
{
+ bool draft = false;
bool linkf = false;
bool preserve = false;
bool retainseqs = false;
int filep = 0;
- int foldp = 0, isdf = 0;
+ int foldp = 0;
bool unlink_msgs = false;
int i, msgnum;
char *cp, *folder = NULL, buf[BUFSIZ];
folder = path (*cp == '+' || *cp == '@' ? cp + 1 : cp,
*cp != '@' ? TFOLDER : TSUBCWF);
continue;
- case DRAFTSW:
+ case DRAFTSW: {
if (filep > NFOLDERS)
die("only %d files allowed!", NFOLDERS);
- isdf = 0;
+ draft = true;
+ int isdf = 0;
files[filep++] = mh_xstrdup(m_draft(NULL, NULL, 1, &isdf));
continue;
+ }
case FILESW:
if (filep > NFOLDERS)
die("only %d files allowed!", NFOLDERS);
if (foldp == 0)
die("no folder specified");
+ mh_managed_folders *mmfs;
+ if (!mh_managed_folders_open(&mmfs))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ mh_managed_folder **const mmf = mh_xmalloc(foldp * sizeof *mmf);
+ *mmf = mh_managed_folder_lookup(mmfs, folders[0].f_name, strlen(folders[0].f_name));
+ if (*mmf != NULL) {
+ if (foldp > 1)
+ die("TODO multiple destination folders not yet supported");
+ if (draft || retainseqs)
+ die("TODO flags not yet supported");
+ if (preserve)
+ die("-preserve not supported by managed folder");
+ if (unlink_msgs)
+ die("-unlink not supported by managed folder");
+ mh_folder_manager_client *mfmc;
+ if (filep > 0) {
+ if (filep > 1)
+ die("TODO multiple files not yet supported");
+ if (!mh_folder_manager_connect(&mfmc, *mmf))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ if (!mh_folder_manager_refile_file(mfmc, files[0], folders[0].f_name))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ /* If -nolink, then "remove" files */
+ if (!linkf)
+ remove_files (filep, filevec);
+ } else {
+ if (linkf)
+ die("TODO -link not yet supported for folder-to-folder refile");
+ if (mh_managed_folder_lookup(mmfs, folder, strlen(folder)) != *mmf)
+ die("TODO refile only supported on one folder manager for now");
+ if (!mh_folder_manager_connect(&mfmc, *mmf))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ if (!mh_folder_manager_refile_move(mfmc, folder, msgs, folders[0].f_name))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ }
+ if (!mh_folder_manager_close(mfmc))
+ die("TODO mh_folder_manager_close");
+ done (0);
+ return 1;
+ }
+
/*
* We are refiling a file to the folders
*/
app_msgarg(&msgs, "cur");
if (!folder)
folder = getfolder (1);
+
strncpy (maildir, m_maildir (folder), sizeof(maildir) - 1);
if (chdir (maildir) == NOTOK)
strncpy (drft, buildsw ? m_maildir ("reply")
: m_draft (dfolder, NULL, NOUSE, &isdf), sizeof(drft) - 1);
+ // TODO(epg): Duplicated from comp.
/* Check if a draft exists */
if (!buildsw && stat (drft, &st) != NOTOK) {
printf ("Draft \"%s\" exists (%ld bytes).", drft, (long) st.st_size);
for (i = LISTDSW; i != YESW;) {
- if (!(argp = read_switch_multiword ("\nDisposition? ",
- isdf ? aqrnl : aqrl)))
+ char *const *words = read_switch_multiword ("\nDisposition? ", isdf ? aqrnl : aqrl);
+ if (words == NULL)
done (1);
- switch (i = smatch (*argp, isdf ? aqrnl : aqrl)) {
+ switch (i = smatch (*words, isdf ? aqrnl : aqrl)) {
case NOSW:
done (0);
case NEWSW:
case YESW:
break;
case LISTDSW:
- showfile (++argp, drft);
+ showfile (++words, drft);
break;
case REFILSW:
- if (refile (++argp, drft) == 0)
+ if (refile (++words, drft) == 0)
i = YESW;
break;
default:
*/
#include "h/mh.h"
+#include "h/managed_folders.h"
#include "sbr/getarguments.h"
#include "sbr/read_yes_or_no_if_tty.h"
#include "sbr/concat.h"
static int rmf(char *);
static void rma (char *);
+static int
+managed_rmf (mh_managed_folder *const mmf, char *const folder)
+{
+ char *const maildir = m_maildir (folder);
+
+ /* chdir is not required for managed folders but may be expected
+ * by external delete hook. */
+ if (chdir (maildir) < 0)
+ adios (maildir, "chdir");
+
+ mh_folder_manager_client *mfmc;
+ if (!mh_folder_manager_connect(&mfmc, mmf))
+ die("TODO: mh_folder_manager_connect");
+
+ /* Run the external delete hook program. */
+ (void)ext_hook("del-hook", maildir, NULL);
+
+ if (!mh_folder_manager_rmf(mfmc, folder))
+ die("TODO mh_folder_manager_rmf");
+ if (!mh_folder_manager_close(mfmc))
+ die("TODO mh_folder_manager_close");
+
+ /*
+ * Remove any relevant private sequences
+ * or attributes from context file.
+ */
+ rma (folder);
+
+ if (chdir ("..") < 0) {
+ advise ("..", "chdir");
+ }
+
+ return OK;
+}
int
main (int argc, char **argv)
free (cp);
}
- if (rmf (folder) == OK) {
+ mh_managed_folders *mmfs;
+ if (!mh_managed_folders_open(&mmfs))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ mh_managed_folder *const mmf = mh_managed_folder_lookup(mmfs, folder, strlen(folder));
+
+ const int status = mmf == NULL ? rmf (folder) : managed_rmf (mmf, folder);
+ if (status == OK) {
char *cfolder = context_find(pfolder);
if (cfolder && strcmp (cfolder, newfolder)) {
printf ("[+%s now current]\n", newfolder);
#include "sbr/print_help.h"
#include "sbr/error.h"
#include "h/done.h"
+#include "h/managed_folders.h"
#include "h/utils.h"
#include "sbr/m_maildir.h"
app_msgarg(&msgs, "cur");
if (!folder)
folder = getfolder (1);
+
+ mh_managed_folders *mmfs;
+ if (!mh_managed_folders_open(&mmfs))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ mh_managed_folder *const mmf = mh_managed_folder_lookup(mmfs, folder, strlen(folder));
+ if (mmf != NULL) {
+ if (!unlink_msgs)
+ die("%s", "managed folder only implemented for -unlink currently");
+ if (rmmproc != NULL)
+ die("%s", "managed folder only implemented without rmmproc currently");
+ mh_folder_manager_client *mfmc;
+ if (!mh_folder_manager_connect(&mfmc, mmf))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ if (!mh_folder_manager_rmm(mfmc, folder, msgs))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ done (0);
+ return 1;
+ }
+
maildir = m_maildir (folder);
if (chdir (maildir) == NOTOK)
#include "sbr/context_save.h"
#include "sbr/context_replace.h"
#include "sbr/context_find.h"
-#include "sbr/brkstring.h"
#include "sbr/ambigsw.h"
#include "sbr/path.h"
#include "sbr/print_version.h"
#include "sbr/seq_getnum.h"
#include "sbr/error.h"
#include "h/fmt_scan.h"
+#include "h/managed_folders.h"
+#include "h/folder.h"
#include "h/tws.h"
#include "h/mts.h"
#include "h/done.h"
int ontty = 0;
int width = -1;
bool revflag = false;
- int i, state, msgnum;
- ivector_t seqnum = ivector_create (0);
+ int state, msgnum;
bool unseen;
- int num_unseen_seq = 0;
- char *cp, *maildir, *file = NULL, *folder = NULL;
+ char *cp, *file = NULL, *folder = NULL;
char *form = NULL, *format = NULL, buf[BUFSIZ];
char **argp, *nfs, **arguments;
struct msgs_array msgs = { 0, 0, NULL };
- struct msgs *mp;
+ const struct msgs *mp;
charstring_t scanl = NULL;
FILE *in;
app_msgarg(&msgs, "all");
if (!folder)
folder = getfolder (1);
- maildir = m_maildir (folder);
-
- if (chdir (maildir) == NOTOK)
- adios (maildir, "unable to change directory to");
-
- /* read folder and create message structure */
- if (!(mp = folder_read (folder, 1)))
- die("unable to read folder %s", folder);
-
- /* check for empty folder */
- if (mp->nummsg == 0)
- die("no messages in %s", folder);
-
- /* parse all the message ranges/sequences and set SELECTED */
- for (msgnum = 0; msgnum < msgs.size; msgnum++)
- if (!m_convert (mp, msgs.msgs[msgnum]))
- done(1);
- seq_setprev (mp); /* set the Previous-Sequence */
-
- context_replace (pfolder, folder); /* update current folder */
- seq_save (mp); /* synchronize message sequences */
- context_save (); /* save the context file */
-
- /*
- * Get the sequence number for each sequence
- * specified by Unseen-Sequence
- */
- if ((cp = context_find (usequence)) && *cp) {
- char **ap, *dp;
-
- dp = mh_xstrdup(cp);
- ap = brkstring (dp, " ", "\n");
- for (i = 0; ap && *ap; i++, ap++)
- ivector_push_back (seqnum, seq_getnum (mp, *ap));
-
- num_unseen_seq = i;
- free(dp);
+ mh_managed_folders *mmfs;
+ if (!mh_managed_folders_open(&mmfs)) {
+ die("%s", mh_managed_folders_get_err_string(mmfs));
}
+ mh_folder *mhf;
+ if (!mh_folder_open(&mhf, folder, mmfs))
+ die("%s", mh_folder_get_err_string(mhf));
+ if (!(mp = mh_folder_select_and_chdir_for_scan(mhf, msgs)))
+ die("%s", mh_folder_get_err_string(mhf));
ontty = isatty (fileno (stdout));
* Check if message is in any sequence given
* by Unseen-Sequence profile entry.
*/
- unseen = false;
- for (i = 0; i < num_unseen_seq; i++) {
- if (in_sequence(mp, ivector_at (seqnum, i), msgnum)) {
- unseen = true;
- break;
- }
- }
+ unseen = mh_folder_message_is_unseen_for_scan(mhf, msgnum);
switch (state = scan (in, msgnum, 0, nfs, width,
msgnum == mp->curmsg, unseen,
}
charstring_free (scanl);
- ivector_free (seqnum);
- folder_free (mp); /* free folder/message structure */
+ mh_folder_close (mhf);
if (clearflag)
nmh_clear_screen ();
#include "sbr/error.h"
#include <fcntl.h>
#include "h/done.h"
+#include "h/managed_folders.h"
+#include "h/folder.h"
#include "h/utils.h"
#ifdef OAUTH_SUPPORT
#include "h/oauth.h"
int msgp = 0, vecp;
int isdf = 0;
int msgnum, status;
- char *cp, *dfolder = NULL, *maildir = NULL;
- char buf[BUFSIZ], **ap, **argp, **arguments, *program;
+ char *cp, *dfolder = NULL;
+ char buf[BUFSIZ], **argp, **arguments, *program;
char *msgs[MAXARGS], **vec;
const char *user = NULL, *saslmech = NULL;
- struct msgs *mp;
struct stat st;
char *auth_svc = NULL;
if ((cp = context_find ("Aliasfile"))) {
char *dp = NULL;
- for (ap = brkstring(dp = mh_xstrdup(cp), " ", "\n"); ap && *ap; ap++) {
+ for (char *const *ap = brkstring(dp = mh_xstrdup(cp), " ", "\n"); ap && *ap; ap++) {
vec[vecp++] = "-alias";
vec[vecp++] = *ap;
}
}
+ /* TODO WHY treat -draftfolder different from profile Draft-Folder?! And
+ * one branch uses m_draft and one doesn't? comp seems to duplicate some
+ * of m_draft too... draft management clearly needs a lot of work. */
if (dfolder == NULL) {
if (msgp == 0) {
msgs[msgp++] = mh_xstrdup(m_draft(NULL, NULL, 1, &isdf));
adios (msgs[0], "unable to stat draft file");
cp = concat ("Use \"", msgs[0], "\"? ", NULL);
for (status = LISTDSW; status != YESW;) {
- if (!(argp = read_switch_multiword (cp, anyl)))
- done (1);
- switch (status = smatch (*argp, anyl)) {
+ char *const *words = read_switch_multiword (cp, anyl);
+ if (words == NULL)
+ done (1);
+ switch (status = smatch (*words, anyl)) {
case NOSW:
done (0);
case YESW:
break;
case LISTDSW:
- showfile (++argp, msgs[0]);
+ showfile (++words, msgs[0]);
break;
default:
inform("say what?");
if (!msgp)
msgs[msgp++] = "cur";
- maildir = m_maildir (dfolder);
-
- if (chdir (maildir) == NOTOK)
- adios (maildir, "unable to change directory to");
-
- /* read folder and create message structure */
- if (!(mp = folder_read (dfolder, 1)))
- die("unable to read folder %s", dfolder);
-
- /* check for empty folder */
- if (mp->nummsg == 0)
- die("no messages in %s", dfolder);
-
- /* parse all the message ranges/sequences and set SELECTED */
- for (msgnum = 0; msgnum < msgp; msgnum++)
- if (!m_convert (mp, msgs[msgnum]))
- done (1);
- seq_setprev (mp); /* set the previous-sequence */
+ mh_managed_folders *mmfs;
+ if (!mh_managed_folders_open(&mmfs))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ mh_folder *mhf;
+ if (!mh_folder_open(&mhf, dfolder, mmfs))
+ die("%s", mh_folder_get_err_string(mhf));
+ const struct msgs *const mp = mh_folder_select_and_chdir_for_send(mhf, msgs, msgp);
+ if (mp == NULL)
+ die("%s", mh_folder_get_err_string(mhf));
for (msgp = 0, msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++) {
if (is_selected (mp, msgnum)) {
msgs[msgp++] = mh_xstrdup(m_name (msgnum));
- unset_exists (mp, msgnum);
}
}
- mp->msgflags |= SEQMOD;
- seq_save (mp);
+ mh_folder_close (mhf);
}
-#ifdef WHATNOW
-go_to_it:
-#endif /* WHATNOW */
-
if ((cp = getenv ("SIGNATURE")) == NULL || *cp == 0)
if ((cp = context_find ("signature")) && *cp)
setenv("SIGNATURE", cp, 1);
status = 0;
- for (msgnum = 0; msgnum < msgp; msgnum++) {
- switch (sendsbr (vec, vecp, program, msgs[msgnum], &st, 1, auth_svc)) {
+ for (int i = 0; i < msgp; i++) {
+ char *const msgnum = msgs[i];
+ switch (sendsbr (vec, vecp, program, msgnum, &st, dfolder == NULL, auth_svc)) {
case DONE:
done (++status);
- /* FALLTHRU */
+ break;
case NOTOK:
status++;
- /* FALLTHRU */
+ break;
case OK:
+ if (dfolder != NULL) {
+ char *const rmm_command = concat ("rmm +", dfolder, " ", msgnum, NULL);
+ const int status = system (rmm_command);
+ free (rmm_command);
+ if (!WIFEXITED (status) || WEXITSTATUS (status) != 0) {
+ adios (msgnum, "failed to rmm after send");
+ }
+ }
break;
}
}
{
int fd2, fd3, msgnum;
char *cp, *folder, *maildir;
- char buffer[BUFSIZ], **ap;
+ char buffer[BUFSIZ];
FILE *fp;
struct msgs *mp;
}
else
fd2 = NOTOK;
- for (ap = brkstring (cp = mh_xstrdup(cp), " ", NULL); *ap; ap++)
+ for (char *const *ap = brkstring (cp = mh_xstrdup(cp), " ", NULL); *ap; ap++)
m_convert (mp, *ap);
free (cp);
if (fd2 != NOTOK)
*/
#include "h/mh.h"
+#include "h/managed_folders.h"
+#include "h/folder.h"
#include "mhlsbr.h"
#include "sbr/m_name.h"
#include "sbr/m_getfld.h"
bool checkmime = true;
bool mime = false;
int isdf = 0, mode = SHOW, msgnum;
- char *cp, *maildir, *file = NULL, *folder = NULL, *proc, *program;
+ char *cp, *file = NULL, *folder = NULL, *proc, *program;
char buf[BUFSIZ], **argp, **arguments;
- struct msgs *mp = NULL;
struct msgs_array msgs = { 0, 0, NULL };
struct msgs_array vec = { 0, 0, NULL }, non_mhl_vec = { 0, 0, NULL };
if (!folder)
folder = getfolder (1);
- maildir = m_maildir (folder);
+ mh_managed_folders *mmfs;
+ if (!mh_managed_folders_open(&mmfs))
+ die("%s", mh_managed_folders_get_err_string(mmfs));
+ mh_folder *mhf;
+ if (!mh_folder_open(&mhf, folder, mmfs))
+ die("%s", mh_folder_get_err_string(mhf));
+ const struct msgs *const mp = mh_folder_select_and_chdir_and_mark_seen(&vec, mhf, msgs);
+ if (mp == NULL)
+ die("%s", mh_folder_get_err_string(mhf));
- if (chdir (maildir) == NOTOK)
- adios (maildir, "unable to change directory to");
-
- /* read folder and create message structure */
- if (!(mp = folder_read (folder, 1)))
- die("unable to read folder %s", folder);
-
- /* check for empty folder */
- if (mp->nummsg == 0)
- die("no messages in %s", folder);
-
- /* parse all the message ranges/sequences and set SELECTED */
- for (msgnum = 0; msgnum < msgs.size; msgnum++)
- if (!m_convert (mp, msgs.msgs[msgnum]))
- done (1);
-
- /*
- * Set the SELECT_UNSEEN bit for all the SELECTED messages,
- * since we will use that as a tag to know which messages
- * to remove from the "unseen" sequence.
- */
- for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++)
- if (is_selected(mp, msgnum))
- set_unseen (mp, msgnum);
-
- seq_setprev (mp); /* set the Previous-Sequence */
- seq_setunseen (mp, 1); /* unset the Unseen-Sequence */
-
- for (msgnum = mp->lowsel; msgnum <= mp->hghsel; msgnum++)
- if (is_selected(mp, msgnum))
- app_msgarg(&vec, mh_xstrdup(m_name (msgnum)));
-
- seq_setcur (mp, mp->hghsel); /* update current message */
- seq_save (mp); /* synchronize sequences */
context_replace (pfolder, folder); /* update current folder */
context_save (); /* save the context file */
done (1);
for (;;) {
+ // read_switch_multiword(_via_readline) return pointer to brkstring's
+ // private data. Writing into it is rude, but as long as we don't free
+ // it, there's no practical harm. This is the least of our problems...
#ifdef READLINE_SUPPORT
- if (!(argp = read_switch_multiword_via_readline(myprompt, aleqs))) {
+ if (!(argp = (char**)read_switch_multiword_via_readline(myprompt, aleqs))) {
#else /* ! READLINE_SUPPORT */
- if (!(argp = read_switch_multiword(myprompt, aleqs))) {
+ if (!(argp = (char**)read_switch_multiword(myprompt, aleqs))) {
#endif /* READLINE_SUPPORT */
(void) m_unlink (LINK);
done (1);
/* allow Aliasfile: profile entry */
if ((cp = context_find ("Aliasfile"))) {
- char **ap, *dp;
-
- dp = mh_xstrdup(cp);
- for (ap = brkstring (dp, " ", "\n"); ap && *ap; ap++) {
+ char *const dp = mh_xstrdup(cp);
+ for (char *const *ap = brkstring (dp, " ", "\n"); ap && *ap; ap++) {
vec[vecp++] = "-alias";
vec[vecp++] = *ap;
}
int vecp = 0;
bool distsw;
char *cp, *dfolder = NULL, *dmsg = NULL;
- char *msg = NULL, **ap, **argp, backup[BUFSIZ];
+ char *msg = NULL, **argp, backup[BUFSIZ];
char buf[BUFSIZ], **arguments, *vec[MAXARGS];
if (nmh_init(argv[0], true, false)) { return 1; }
if ((cp = context_find ("Aliasfile"))) {
char *dp = NULL;
- for (ap = brkstring(dp = mh_xstrdup(cp), " ", "\n"); ap && *ap; ap++) {
+ for (char *const *ap = brkstring(dp = mh_xstrdup(cp), " ", "\n"); ap && *ap; ap++) {
vec[vecp++] = "-alias";
vec[vecp++] = *ap;
}