trim: cut what no longer has a caller or a reader
A sweep for dead weight, taking the context seriously: three people, one
tailnet, one proxy.

Gone entirely:

- tasks/probe.rake. It seeded five probe cards for a real client to
  edit, still setting Tailscale-User-Login and Tailscale-User-Name and
  citing ProTacts::TailscaleAuth, none of which have existed since the
  login moved to Remote-User. Every question it asks is answered in
  docs/apple-contacts.md, which now describes the cards rather than
  naming tasks that are gone.
- monica/README.md, a password and an API token for a Monica instance
  under the gitignored servers/. The credentials stay in the history;
  they are a throwaway local container's and worth rotating anyway.
- VCard.fold. Nothing served through it: a card goes out as the bytes
  that were stored, and the web editor writes the one line it changed.
  The parser test that needed folded input now folds its own, beside
  the unescape and normalize oracles it already keeps.
- ProxyAuth's RFC 2047 decoding, and the vendored rfc2047.txt behind
  it. It read what `tailscale serve` Q-encodes; the deploy is Caddy's
  header_up, which copies the value through, and a tailnet login is an
  email address. The UTF-8 guard stays, so bytes that are not text name
  nobody instead of raising at a bind several layers down.
- COMMIT_SHA, CHANGE_ID and BUILD_DATE, passed from ci.yml into the
  image and read by nothing. VERSION already carries the build time and
  the short sha.

Fixed rather than cut:

- config.ru dropped Tailscale-User-* from Sentry events while letting
  Remote-User through, so the header that actually carries identity was
  the one being sent. ProxyAuth::HEADER now names it once, for the
  scrubber, the 401, and the read.
- log/unhandled is called the exchange log in the four comments outside
  docs/plans that still named it.
- AGENTS.md said escape and fold had no caller; escape has had ten in
  admin/card_form.rb since the editor landed.
- docs/apple-contacts.md and the README argued the reference-server case
  twice. The README keeps it.

Verified with steep (clean) and a prism parse of every Ruby file; the
test suite needs Ruby 4 and this container has 3.3, so it has not run.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UXvcTFuXPr8ecpyD99h33M
change
commit 7ff5b05cb97b2ee341a7cb584c67415a257f0027
author Claude <noreply@anthropic.com>
date
parent kwzzzqwk
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 180131c..8dabe86 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -62,8 +62,6 @@ jobs:
           password: ${{ secrets.GITHUB_TOKEN }}
       - id: short-sha
         run: echo "sha=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
-      - id: change-id
-        run: echo "id=$(git cat-file -p HEAD | grep '^change-id ' | cut -d' ' -f2)" >> "$GITHUB_OUTPUT"
       - uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
         id: meta
         with:
@@ -78,9 +76,6 @@ jobs:
           tags: ${{ steps.meta.outputs.tags }}
           labels: ${{ steps.meta.outputs.labels }}
           build-args: |
-            COMMIT_SHA=${{ steps.short-sha.outputs.sha }}
-            CHANGE_ID=${{ steps.change-id.outputs.id }}
-            BUILD_DATE=${{ github.event.head_commit.timestamp }}
             VERSION=${{ steps.meta.outputs.version }}
           cache-from: type=gha
           cache-to: type=gha,mode=max
diff --git a/AGENTS.md b/AGENTS.md
index cdef453..6106d7d 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -214,9 +214,12 @@ not a fixture; edit a `.vcf` to change what the replay serves.
   unknown key meant a typo silently losing data; it now reads stored
   cards, where an unknown property means a client using the spec, and
   RFC 6352 section 6.3.2.2 requires keeping it.
-- `escape` and `fold` in `vcard.rb` have no caller yet. They are the
-  writer's half of the module, kept deliberately for PUT and the web
-  editor, and tested directly rather than through anything that serves.
+- `escape` and `header_of` in `vcard.rb` are the writer's half of the
+  module, and the web editor's saves are rebuilt through them
+  (`admin/card_form.rb`). There is no `fold` beside them: a served card
+  is stored bytes going out untouched, and the editor writes the one
+  line it changed, so nothing has ever had a logical line to fold.
+  `Parser.unfold` is the reading half and does have callers.
 - `vcard` names a `VCard` and nothing else, and a card with no
   qualifier to give it is named `vcard`. A `vcard` holding a card's
   bytes is the confusion the rule exists to stop: the two sit one
diff --git a/Dockerfile b/Dockerfile
index 26b2808..e06773b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,15 +11,11 @@ COPY . .
 
 FROM ruby:4.0-slim
 
-ARG COMMIT_SHA
-ARG CHANGE_ID
-ARG BUILD_DATE
 # The image's own tag, and the release's name (see ci.yml) — the one
-# build fact the app reads back out, in the admin footer.
+# build fact the app reads back out, in the admin footer. It carries the
+# build time and the short sha, so nothing else about the build has to
+# be passed in beside it.
 ARG VERSION
-ENV COMMIT_SHA=${COMMIT_SHA}
-ENV CHANGE_ID=${CHANGE_ID}
-ENV BUILD_DATE=${BUILD_DATE}
 ENV VERSION=${VERSION}
 # rackup defaults to development, which wraps the app in Rack::Lint and
 # ShowExceptions.
diff --git a/config.ru b/config.ru
index 60172fd..5d7615c 100644
--- a/config.ru
+++ b/config.ru
@@ -32,9 +32,10 @@ Sentry.init do |sentry|
   sentry.send_default_pii = false
 
   # Headers are sent regardless, on transactions as well as errors, and
-  # the tailnet identity is not worth sending.
+  # the tailnet identity is not worth sending. Named off ProxyAuth, so
+  # the header that gets dropped is the one the app actually reads.
   drop_identity = lambda do |event, _hint|
-    event.request&.headers&.reject! { |name, _| name.start_with?("Tailscale-User-") }
+    event.request&.headers&.reject! { |name, _| name.casecmp?(ProTacts::ProxyAuth::HEADER) }
     event
   end
   sentry.before_send = drop_identity
diff --git a/docs/apple-contacts.md b/docs/apple-contacts.md
index a717ab2..99345d7 100644
--- a/docs/apple-contacts.md
+++ b/docs/apple-contacts.md
@@ -243,8 +243,8 @@ reports every untouched property as modified. Verified 2026-08-24.
 
 ## An address type the client cannot model becomes a custom label
 
-`rake probe:types` sends three addresses and asks for one relabel — 1
-Modeled Way from Home to Work — to answer what a round trip does to `TYPE`:
+A probe card of three addresses, with one relabel asked for — 1 Modeled
+Way from Home to Work — answers what a round trip does to `TYPE`:
 
 | Sent | Came back |
 |---|---|
@@ -274,9 +274,8 @@ Verified 2026-09-09 against macOS 26.5.1 (AddressBookCore/2732.600.11).
 ## An annotation survives only on its own line
 
 A server that wants to mark a line — to say which group lent an address,
-say — gets one form that works and four that do not. `rake
-probe:annotations` sends a card carrying five, and one client edit
-answers all of them:
+say — gets one form that works and four that do not. A probe card
+carrying all five, and one client edit, answers all of them:
 
 | Form | Sent | Came back |
 |---|---|---|
@@ -298,9 +297,9 @@ third line of that group behind.
 
 That probe chose its own numbering — `item8` and `item9`, picked to dodge
 collisions — so it could not tell a broken binding from numbering the
-client disliked. `rake probe:renumber` settles it with two groups spelled
-the way a client spells its own, dense and in document order, each
-anchored by an `X-ABLabel`:
+client disliked. A second card settles it with two groups spelled the
+way a client spells its own, dense and in document order, each anchored
+by an `X-ABLabel`:
 
 | Sent | Came back |
 |---|---|
@@ -485,10 +484,10 @@ stored. Verified 2026-09-11 against iOS 26.6.2 (dataaccessd/1.0).
 
 ## Only the last of two notes survives
 
-`rake probe:notes` serves a card with two `NOTE` lines, the shape a group
-member with a note of its own is served in. RFC 6350 section 6.7.2 allows
-any number of them; Contacts keeps one. The Note field shows only the last,
-and the card it writes back after an unrelated edit carries only that one:
+A probe card with two `NOTE` lines, the shape a group member with a note
+of its own is served in. RFC 6350 section 6.7.2 allows any number of
+them; Contacts keeps one. The Note field shows only the last, and the
+card it writes back after an unrelated edit carries only that one:
 
 ```
 NOTE:Own note: gate code 1854.           ->  (dropped)
@@ -503,10 +502,10 @@ survives is the group's and the one lost is the member's. Verified
 
 ## Categories survive a round trip but are never shown
 
-`rake probe:categories` serves two `CATEGORIES` lines (RFC 2426 section
-3.6.1), one of them a comma list. Contacts shows neither anywhere on the
-card, and keeps both anyway. The card it writes back after an unrelated
-edit carries both, values byte-identical and the list unsplit, in reverse
+A probe card with two `CATEGORIES` lines (RFC 2426 section 3.6.1), one
+of them a comma list. Contacts shows neither anywhere on the card, and
+keeps both anyway. The card it writes back after an unrelated edit
+carries both, values byte-identical and the list unsplit, in reverse
 order:
 
 ```
@@ -556,7 +555,7 @@ Contacts (`dataaccessd/1.0`) needs nothing it leaves out. On 2026-09-11,
 iOS 26.6.2 discovered the account on the dev server and synced it. Its
 first multiget asked for all 20 cards and got 20 back, two cards seeded
 afterwards reached the phone on a later poll, and every response was a
-200 or 207, with nothing captured in `log/unhandled`. An earlier session,
+200 or 207, with nothing landing in the exchange log. An earlier session,
 iOS 26.6.1 on 2026-09-08, also edited a card and deleted one.
 
 iOS differs from macOS on the wire rather than in what it needs:
@@ -567,11 +566,7 @@ difference as a fixture the test suite replays.
 
 ## Reference implementations to compare against
 
-`servers/` runs Baikal, Radicale, and Monica behind mitmproxy, because those
-servers cannot be made to log what we need. Monica is the one confirmed
-working with macOS Contacts here. When pro-tacts and a working server
-disagree, the diff between two recordings of the same client action is the
-fastest way to find out why.
-
-pro-tacts itself is debugged through its own debug logging mode rather than
-through a proxy.
+The README's "Reference implementations" section covers what `servers/`
+runs and which of them to trust. What it is for is this: when pro-tacts
+and a working server disagree, the diff between two recordings of the
+same client action is the fastest way to find out why.
diff --git a/docs/rfcs/README.md b/docs/rfcs/README.md
index 31278a1..bc4bc91 100644
--- a/docs/rfcs/README.md
+++ b/docs/rfcs/README.md
@@ -20,7 +20,6 @@ handler that implements each one, and `lib/pro_tacts/vcard.rb` cites RFC
 | [2426](rfc2426.txt) | vCard 3.0 | The card format itself; the version Apple clients speak |
 | [2425](rfc2425.txt) | A MIME Content-Type for Directory Information | The value types RFC 2426 defers to rather than define, `date` among them |
 | [6350](rfc6350.txt) | vCard 4.0 | Nothing here implements it; kept for comparison |
-| [2047](rfc2047.txt) | MIME Part Three: Message Header Extensions | The encoded-words `tailscale serve` writes identity headers in |
 
 One property the server sends has no RFC. `getctag` is an Apple
 CalendarServer extension in the `http://calendarserver.org/ns/`
diff --git a/docs/rfcs/rfc2047.txt b/docs/rfcs/rfc2047.txt
deleted file mode 100644
index ff9a744..0000000
--- a/docs/rfcs/rfc2047.txt
+++ /dev/null
@@ -1,843 +0,0 @@
-
-
-
-
-
-
-Network Working Group                                           K. Moore
-Request for Comments: 2047                       University of Tennessee
-Obsoletes: 1521, 1522, 1590                                November 1996
-Category: Standards Track
-
-
-        MIME (Multipurpose Internet Mail Extensions) Part Three:
-              Message Header Extensions for Non-ASCII Text
-
-Status of this Memo
-
-   This document specifies an Internet standards track protocol for the
-   Internet community, and requests discussion and suggestions for
-   improvements.  Please refer to the current edition of the "Internet
-   Official Protocol Standards" (STD 1) for the standardization state
-   and status of this protocol.  Distribution of this memo is unlimited.
-
-Abstract
-
-   STD 11, RFC 822, defines a message representation protocol specifying
-   considerable detail about US-ASCII message headers, and leaves the
-   message content, or message body, as flat US-ASCII text.  This set of
-   documents, collectively called the Multipurpose Internet Mail
-   Extensions, or MIME, redefines the format of messages to allow for
-
-   (1) textual message bodies in character sets other than US-ASCII,
-
-   (2) an extensible set of different formats for non-textual message
-       bodies,
-
-   (3) multi-part message bodies, and
-
-   (4) textual header information in character sets other than US-ASCII.
-
-   These documents are based on earlier work documented in RFC 934, STD
-   11, and RFC 1049, but extends and revises them.  Because RFC 822 said
-   so little about message bodies, these documents are largely
-   orthogonal to (rather than a revision of) RFC 822.
-
-   This particular document is the third document in the series.  It
-   describes extensions to RFC 822 to allow non-US-ASCII text data in
-   Internet mail header fields.
-
-
-
-
-
-
-
-
-
-Moore                       Standards Track                     [Page 1]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-   Other documents in this series include:
-
-   + RFC 2045, which specifies the various headers used to describe
-     the structure of MIME messages.
-
-   + RFC 2046, which defines the general structure of the MIME media
-     typing system and defines an initial set of media types,
-
-   + RFC 2048, which specifies various IANA registration procedures
-     for MIME-related facilities, and
-
-   + RFC 2049, which describes MIME conformance criteria and
-     provides some illustrative examples of MIME message formats,
-     acknowledgements, and the bibliography.
-
-   These documents are revisions of RFCs 1521, 1522, and 1590, which
-   themselves were revisions of RFCs 1341 and 1342.  An appendix in RFC
-   2049 describes differences and changes from previous versions.
-
-1. Introduction
-
-   RFC 2045 describes a mechanism for denoting textual body parts which
-   are coded in various character sets, as well as methods for encoding
-   such body parts as sequences of printable US-ASCII characters.  This
-   memo describes similar techniques to allow the encoding of non-ASCII
-   text in various portions of a RFC 822 [2] message header, in a manner
-   which is unlikely to confuse existing message handling software.
-
-   Like the encoding techniques described in RFC 2045, the techniques
-   outlined here were designed to allow the use of non-ASCII characters
-   in message headers in a way which is unlikely to be disturbed by the
-   quirks of existing Internet mail handling programs.  In particular,
-   some mail relaying programs are known to (a) delete some message
-   header fields while retaining others, (b) rearrange the order of
-   addresses in To or Cc fields, (c) rearrange the (vertical) order of
-   header fields, and/or (d) "wrap" message headers at different places
-   than those in the original message.  In addition, some mail reading
-   programs are known to have difficulty correctly parsing message
-   headers which, while legal according to RFC 822, make use of
-   backslash-quoting to "hide" special characters such as "<", ",", or
-   ":", or which exploit other infrequently-used features of that
-   specification.
-
-   While it is unfortunate that these programs do not correctly
-   interpret RFC 822 headers, to "break" these programs would cause
-   severe operational problems for the Internet mail system.  The
-   extensions described in this memo therefore do not rely on little-
-   used features of RFC 822.
-
-
-
-Moore                       Standards Track                     [Page 2]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-   Instead, certain sequences of "ordinary" printable ASCII characters
-   (known as "encoded-words") are reserved for use as encoded data.  The
-   syntax of encoded-words is such that they are unlikely to
-   "accidentally" appear as normal text in message headers.
-   Furthermore, the characters used in encoded-words are restricted to
-   those which do not have special meanings in the context in which the
-   encoded-word appears.
-
-   Generally, an "encoded-word" is a sequence of printable ASCII
-   characters that begins with "=?", ends with "?=", and has two "?"s in
-   between.  It specifies a character set and an encoding method, and
-   also includes the original text encoded as graphic ASCII characters,
-   according to the rules for that encoding method.
-
-   A mail composer that implements this specification will provide a
-   means of inputting non-ASCII text in header fields, but will
-   translate these fields (or appropriate portions of these fields) into
-   encoded-words before inserting them into the message header.
-
-   A mail reader that implements this specification will recognize
-   encoded-words when they appear in certain portions of the message
-   header.  Instead of displaying the encoded-word "as is", it will
-   reverse the encoding and display the original text in the designated
-   character set.
-
-NOTES
-
-   This memo relies heavily on notation and terms defined RFC 822 and
-   RFC 2045.  In particular, the syntax for the ABNF used in this memo
-   is defined in RFC 822, as well as many of the terminal or nonterminal
-   symbols from RFC 822 are used in the grammar for the header
-   extensions defined here.  Among the symbols defined in RFC 822 and
-   referenced in this memo are: 'addr-spec', 'atom', 'CHAR', 'comment',
-   'CTLs', 'ctext', 'linear-white-space', 'phrase', 'quoted-pair'.
-   'quoted-string', 'SPACE', and 'word'.  Successful implementation of
-   this protocol extension requires careful attention to the RFC 822
-   definitions of these terms.
-
-   When the term "ASCII" appears in this memo, it refers to the "7-Bit
-   American Standard Code for Information Interchange", ANSI X3.4-1986.
-   The MIME charset name for this character set is "US-ASCII".  When not
-   specifically referring to the MIME charset name, this document uses
-   the term "ASCII", both for brevity and for consistency with RFC 822.
-   However, implementors are warned that the character set name must be
-   spelled "US-ASCII" in MIME message and body part headers.
-
-
-
-
-
-
-Moore                       Standards Track                     [Page 3]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-   This memo specifies a protocol for the representation of non-ASCII
-   text in message headers.  It specifically DOES NOT define any
-   translation between "8-bit headers" and pure ASCII headers, nor is
-   any such translation assumed to be possible.
-
-2. Syntax of encoded-words
-
-   An 'encoded-word' is defined by the following ABNF grammar.  The
-   notation of RFC 822 is used, with the exception that white space
-   characters MUST NOT appear between components of an 'encoded-word'.
-
-   encoded-word = "=?" charset "?" encoding "?" encoded-text "?="
-
-   charset = token    ; see section 3
-
-   encoding = token   ; see section 4
-
-   token = 1*<Any CHAR except SPACE, CTLs, and especials>
-
-   especials = "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "
-               <"> / "/" / "[" / "]" / "?" / "." / "="
-
-   encoded-text = 1*<Any printable ASCII character other than "?"
-                     or SPACE>
-                  ; (but see "Use of encoded-words in message
-                  ; headers", section 5)
-
-   Both 'encoding' and 'charset' names are case-independent.  Thus the
-   charset name "ISO-8859-1" is equivalent to "iso-8859-1", and the
-   encoding named "Q" may be spelled either "Q" or "q".
-
-   An 'encoded-word' may not be more than 75 characters long, including
-   'charset', 'encoding', 'encoded-text', and delimiters.  If it is
-   desirable to encode more text than will fit in an 'encoded-word' of
-   75 characters, multiple 'encoded-word's (separated by CRLF SPACE) may
-   be used.
-
-   While there is no limit to the length of a multiple-line header
-   field, each line of a header field that contains one or more
-   'encoded-word's is limited to 76 characters.
-
-   The length restrictions are included both to ease interoperability
-   through internetwork mail gateways, and to impose a limit on the
-   amount of lookahead a header parser must employ (while looking for a
-   final ?= delimiter) before it can decide whether a token is an
-   "encoded-word" or something else.
-
-
-
-
-
-Moore                       Standards Track                     [Page 4]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-   IMPORTANT: 'encoded-word's are designed to be recognized as 'atom's
-   by an RFC 822 parser.  As a consequence, unencoded white space
-   characters (such as SPACE and HTAB) are FORBIDDEN within an
-   'encoded-word'.  For example, the character sequence
-
-      =?iso-8859-1?q?this is some text?=
-
-   would be parsed as four 'atom's, rather than as a single 'atom' (by
-   an RFC 822 parser) or 'encoded-word' (by a parser which understands
-   'encoded-words').  The correct way to encode the string "this is some
-   text" is to encode the SPACE characters as well, e.g.
-
-      =?iso-8859-1?q?this=20is=20some=20text?=
-
-   The characters which may appear in 'encoded-text' are further
-   restricted by the rules in section 5.
-
-3. Character sets
-
-   The 'charset' portion of an 'encoded-word' specifies the character
-   set associated with the unencoded text.  A 'charset' can be any of
-   the character set names allowed in an MIME "charset" parameter of a
-   "text/plain" body part, or any character set name registered with
-   IANA for use with the MIME text/plain content-type.
-
-   Some character sets use code-switching techniques to switch between
-   "ASCII mode" and other modes.  If unencoded text in an 'encoded-word'
-   contains a sequence which causes the charset interpreter to switch
-   out of ASCII mode, it MUST contain additional control codes such that
-   ASCII mode is again selected at the end of the 'encoded-word'.  (This
-   rule applies separately to each 'encoded-word', including adjacent
-   'encoded-word's within a single header field.)
-
-   When there is a possibility of using more than one character set to
-   represent the text in an 'encoded-word', and in the absence of
-   private agreements between sender and recipients of a message, it is
-   recommended that members of the ISO-8859-* series be used in
-   preference to other character sets.
-
-4. Encodings
-
-   Initially, the legal values for "encoding" are "Q" and "B".  These
-   encodings are described below.  The "Q" encoding is recommended for
-   use when most of the characters to be encoded are in the ASCII
-   character set; otherwise, the "B" encoding should be used.
-   Nevertheless, a mail reader which claims to recognize 'encoded-word's
-   MUST be able to accept either encoding for any character set which it
-   supports.
-
-
-
-Moore                       Standards Track                     [Page 5]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-   Only a subset of the printable ASCII characters may be used in
-   'encoded-text'.  Space and tab characters are not allowed, so that
-   the beginning and end of an 'encoded-word' are obvious.  The "?"
-   character is used within an 'encoded-word' to separate the various
-   portions of the 'encoded-word' from one another, and thus cannot
-   appear in the 'encoded-text' portion.  Other characters are also
-   illegal in certain contexts.  For example, an 'encoded-word' in a
-   'phrase' preceding an address in a From header field may not contain
-   any of the "specials" defined in RFC 822.  Finally, certain other
-   characters are disallowed in some contexts, to ensure reliability for
-   messages that pass through internetwork mail gateways.
-
-   The "B" encoding automatically meets these requirements.  The "Q"
-   encoding allows a wide range of printable characters to be used in
-   non-critical locations in the message header (e.g., Subject), with
-   fewer characters available for use in other locations.
-
-4.1. The "B" encoding
-
-   The "B" encoding is identical to the "BASE64" encoding defined by RFC
-   2045.
-
-4.2. The "Q" encoding
-
-   The "Q" encoding is similar to the "Quoted-Printable" content-
-   transfer-encoding defined in RFC 2045.  It is designed to allow text
-   containing mostly ASCII characters to be decipherable on an ASCII
-   terminal without decoding.
-
-   (1) Any 8-bit value may be represented by a "=" followed by two
-       hexadecimal digits.  For example, if the character set in use
-       were ISO-8859-1, the "=" character would thus be encoded as
-       "=3D", and a SPACE by "=20".  (Upper case should be used for
-       hexadecimal digits "A" through "F".)
-
-   (2) The 8-bit hexadecimal value 20 (e.g., ISO-8859-1 SPACE) may be
-       represented as "_" (underscore, ASCII 95.).  (This character may
-       not pass through some internetwork mail gateways, but its use
-       will greatly enhance readability of "Q" encoded data with mail
-       readers that do not support this encoding.)  Note that the "_"
-       always represents hexadecimal 20, even if the SPACE character
-       occupies a different code position in the character set in use.
-
-   (3) 8-bit values which correspond to printable ASCII characters other
-       than "=", "?", and "_" (underscore), MAY be represented as those
-       characters.  (But see section 5 for restrictions.)  In
-       particular, SPACE and TAB MUST NOT be represented as themselves
-       within encoded words.
-
-
-
-Moore                       Standards Track                     [Page 6]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-5. Use of encoded-words in message headers
-
-   An 'encoded-word' may appear in a message header or body part header
-   according to the following rules:
-
-(1) An 'encoded-word' may replace a 'text' token (as defined by RFC 822)
-    in any Subject or Comments header field, any extension message
-    header field, or any MIME body part field for which the field body
-    is defined as '*text'.  An 'encoded-word' may also appear in any
-    user-defined ("X-") message or body part header field.
-
-    Ordinary ASCII text and 'encoded-word's may appear together in the
-    same header field.  However, an 'encoded-word' that appears in a
-    header field defined as '*text' MUST be separated from any adjacent
-    'encoded-word' or 'text' by 'linear-white-space'.
-
-(2) An 'encoded-word' may appear within a 'comment' delimited by "(" and
-    ")", i.e., wherever a 'ctext' is allowed.  More precisely, the RFC
-    822 ABNF definition for 'comment' is amended as follows:
-
-    comment = "(" *(ctext / quoted-pair / comment / encoded-word) ")"
-
-    A "Q"-encoded 'encoded-word' which appears in a 'comment' MUST NOT
-    contain the characters "(", ")" or "
-    'encoded-word' that appears in a 'comment' MUST be separated from
-    any adjacent 'encoded-word' or 'ctext' by 'linear-white-space'.
-
-    It is important to note that 'comment's are only recognized inside
-    "structured" field bodies.  In fields whose bodies are defined as
-    '*text', "(" and ")" are treated as ordinary characters rather than
-    comment delimiters, and rule (1) of this section applies.  (See RFC
-    822, sections 3.1.2 and 3.1.3)
-
-(3) As a replacement for a 'word' entity within a 'phrase', for example,
-    one that precedes an address in a From, To, or Cc header.  The ABNF
-    definition for 'phrase' from RFC 822 thus becomes:
-
-    phrase = 1*( encoded-word / word )
-
-    In this case the set of characters that may be used in a "Q"-encoded
-    'encoded-word' is restricted to: <upper and lower case ASCII
-    letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_"
-    (underscore, ASCII 95.)>.  An 'encoded-word' that appears within a
-    'phrase' MUST be separated from any adjacent 'word', 'text' or
-    'special' by 'linear-white-space'.
-
-
-
-
-
-
-Moore                       Standards Track                     [Page 7]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-   These are the ONLY locations where an 'encoded-word' may appear.  In
-   particular:
-
-   + An 'encoded-word' MUST NOT appear in any portion of an 'addr-spec'.
-
-   + An 'encoded-word' MUST NOT appear within a 'quoted-string'.
-
-   + An 'encoded-word' MUST NOT be used in a Received header field.
-
-   + An 'encoded-word' MUST NOT be used in parameter of a MIME
-     Content-Type or Content-Disposition field, or in any structured
-     field body except within a 'comment' or 'phrase'.
-
-   The 'encoded-text' in an 'encoded-word' must be self-contained;
-   'encoded-text' MUST NOT be continued from one 'encoded-word' to
-   another.  This implies that the 'encoded-text' portion of a "B"
-   'encoded-word' will be a multiple of 4 characters long; for a "Q"
-   'encoded-word', any "=" character that appears in the 'encoded-text'
-   portion will be followed by two hexadecimal characters.
-
-   Each 'encoded-word' MUST encode an integral number of octets.  The
-   'encoded-text' in each 'encoded-word' must be well-formed according
-   to the encoding specified; the 'encoded-text' may not be continued in
-   the next 'encoded-word'.  (For example, "=?charset?Q?=?=
-   =?charset?Q?AB?=" would be illegal, because the two hex digits "AB"
-   must follow the "=" in the same 'encoded-word'.)
-
-   Each 'encoded-word' MUST represent an integral number of characters.
-   A multi-octet character may not be split across adjacent 'encoded-
-   word's.
-
-   Only printable and white space character data should be encoded using
-   this scheme.  However, since these encoding schemes allow the
-   encoding of arbitrary octet values, mail readers that implement this
-   decoding should also ensure that display of the decoded data on the
-   recipient's terminal will not cause unwanted side-effects.
-
-   Use of these methods to encode non-textual data (e.g., pictures or
-   sounds) is not defined by this memo.  Use of 'encoded-word's to
-   represent strings of purely ASCII characters is allowed, but
-   discouraged.  In rare cases it may be necessary to encode ordinary
-   text that looks like an 'encoded-word'.
-
-
-
-
-
-
-
-
-
-Moore                       Standards Track                     [Page 8]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-6. Support of 'encoded-word's by mail readers
-
-6.1. Recognition of 'encoded-word's in message headers
-
-   A mail reader must parse the message and body part headers according
-   to the rules in RFC 822 to correctly recognize 'encoded-word's.
-
-   'encoded-word's are to be recognized as follows:
-
-   (1) Any message or body part header field defined as '*text', or any
-       user-defined header field, should be parsed as follows: Beginning
-       at the start of the field-body and immediately following each
-       occurrence of 'linear-white-space', each sequence of up to 75
-       printable characters (not containing any 'linear-white-space')
-       should be examined to see if it is an 'encoded-word' according to
-       the syntax rules in section 2.  Any other sequence of printable
-       characters should be treated as ordinary ASCII text.
-
-   (2) Any header field not defined as '*text' should be parsed
-       according to the syntax rules for that header field.  However,
-       any 'word' that appears within a 'phrase' should be treated as an
-       'encoded-word' if it meets the syntax rules in section 2.
-       Otherwise it should be treated as an ordinary 'word'.
-
-   (3) Within a 'comment', any sequence of up to 75 printable characters
-       (not containing 'linear-white-space'), that meets the syntax
-       rules in section 2, should be treated as an 'encoded-word'.
-       Otherwise it should be treated as normal comment text.
-
-   (4) A MIME-Version header field is NOT required to be present for
-       'encoded-word's to be interpreted according to this
-       specification.  One reason for this is that the mail reader is
-       not expected to parse the entire message header before displaying
-       lines that may contain 'encoded-word's.
-
-6.2. Display of 'encoded-word's
-
-   Any 'encoded-word's so recognized are decoded, and if possible, the
-   resulting unencoded text is displayed in the original character set.
-
-   NOTE: Decoding and display of encoded-words occurs *after* a
-   structured field body is parsed into tokens.  It is therefore
-   possible to hide 'special' characters in encoded-words which, when
-   displayed, will be indistinguishable from 'special' characters in the
-   surrounding text.  For this and other reasons, it is NOT generally
-   possible to translate a message header containing 'encoded-word's to
-   an unencoded form which can be parsed by an RFC 822 mail reader.
-
-
-
-
-Moore                       Standards Track                     [Page 9]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-   When displaying a particular header field that contains multiple
-   'encoded-word's, any 'linear-white-space' that separates a pair of
-   adjacent 'encoded-word's is ignored.  (This is to allow the use of
-   multiple 'encoded-word's to represent long strings of unencoded text,
-   without having to separate 'encoded-word's where spaces occur in the
-   unencoded text.)
-
-   In the event other encodings are defined in the future, and the mail
-   reader does not support the encoding used, it may either (a) display
-   the 'encoded-word' as ordinary text, or (b) substitute an appropriate
-   message indicating that the text could not be decoded.
-
-   If the mail reader does not support the character set used, it may
-   (a) display the 'encoded-word' as ordinary text (i.e., as it appears
-   in the header), (b) make a "best effort" to display using such
-   characters as are available, or (c) substitute an appropriate message
-   indicating that the decoded text could not be displayed.
-
-   If the character set being used employs code-switching techniques,
-   display of the encoded text implicitly begins in "ASCII mode".  In
-   addition, the mail reader must ensure that the output device is once
-   again in "ASCII mode" after the 'encoded-word' is displayed.
-
-6.3. Mail reader handling of incorrectly formed 'encoded-word's
-
-   It is possible that an 'encoded-word' that is legal according to the
-   syntax defined in section 2, is incorrectly formed according to the
-   rules for the encoding being used.   For example:
-
-   (1) An 'encoded-word' which contains characters which are not legal
-       for a particular encoding (for example, a "-" in the "B"
-       encoding, or a SPACE or HTAB in either the "B" or "Q" encoding),
-       is incorrectly formed.
-
-   (2) Any 'encoded-word' which encodes a non-integral number of
-       characters or octets is incorrectly formed.
-
-   A mail reader need not attempt to display the text associated with an
-   'encoded-word' that is incorrectly formed.  However, a mail reader
-   MUST NOT prevent the display or handling of a message because an
-   'encoded-word' is incorrectly formed.
-
-7. Conformance
-
-   A mail composing program claiming compliance with this specification
-   MUST ensure that any string of non-white-space printable ASCII
-   characters within a '*text' or '*ctext' that begins with "=?" and
-   ends with "?=" be a valid 'encoded-word'.  ("begins" means: at the
-
-
-
-Moore                       Standards Track                    [Page 10]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-   start of the field-body, immediately following 'linear-white-space',
-   or immediately following a "(" for an 'encoded-word' within '*ctext';
-   "ends" means: at the end of the field-body, immediately preceding
-   'linear-white-space', or immediately preceding a ")" for an
-   'encoded-word' within '*ctext'.)  In addition, any 'word' within a
-   'phrase' that begins with "=?" and ends with "?=" must be a valid
-   'encoded-word'.
-
-   A mail reading program claiming compliance with this specification
-   must be able to distinguish 'encoded-word's from 'text', 'ctext', or
-   'word's, according to the rules in section 6, anytime they appear in
-   appropriate places in message headers.  It must support both the "B"
-   and "Q" encodings for any character set which it supports.  The
-   program must be able to display the unencoded text if the character
-   set is "US-ASCII".  For the ISO-8859-* character sets, the mail
-   reading program must at least be able to display the characters which
-   are also in the ASCII set.
-
-8. Examples
-
-   The following are examples of message headers containing 'encoded-
-   word's:
-
-   From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>
-   To: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>
-   CC: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>
-   Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=
-    =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=
-
-      Note: In the first 'encoded-word' of the Subject field above, the
-      last "=" at the end of the 'encoded-text' is necessary because each
-      'encoded-word' must be self-contained (the "=" character completes a
-      group of 4 base64 characters representing 2 octets).  An additional
-      octet could have been encoded in the first 'encoded-word' (so that
-      the encoded-word would contain an exact multiple of 3 encoded
-      octets), except that the second 'encoded-word' uses a different
-      'charset' than the first one.
-
-   From: =?ISO-8859-1?Q?Olle_J=E4rnefors?= <ojarnef@admin.kth.se>
-   To: ietf-822@dimacs.rutgers.edu, ojarnef@admin.kth.se
-   Subject: Time for ISO 10646?
-
-   To: Dave Crocker <dcrocker@mordor.stanford.edu>
-   Cc: ietf-822@dimacs.rutgers.edu, paf@comsol.se
-   From: =?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= <paf@nada.kth.se>
-   Subject: Re: RFC-HDR care and feeding
-
-
-
-
-
-Moore                       Standards Track                    [Page 11]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-   From: Nathaniel Borenstein <nsb@thumper.bellcore.com>
-         (=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)
-   To: Greg Vaudreuil <gvaudre@NRI.Reston.VA.US>, Ned Freed
-      <ned@innosoft.com>, Keith Moore <moore@cs.utk.edu>
-   Subject: Test of new header generator
-   MIME-Version: 1.0
-   Content-type: text/plain; charset=ISO-8859-1
-
-   The following examples illustrate how text containing 'encoded-word's
-   which appear in a structured field body.  The rules are slightly
-   different for fields defined as '*text' because "(" and ")" are not
-   recognized as 'comment' delimiters.  [Section 5, paragraph (1)].
-
-   In each of the following examples, if the same sequence were to occur
-   in a '*text' field, the "displayed as" form would NOT be treated as
-   encoded words, but be identical to the "encoded form".  This is
-   because each of the encoded-words in the following examples is
-   adjacent to a "(" or ")" character.
-
-   encoded form                                displayed as
-   ---------------------------------------------------------------------
-   (=?ISO-8859-1?Q?a?=)                        (a)
-
-   (=?ISO-8859-1?Q?a?= b)                      (a b)
-
-           Within a 'comment', white space MUST appear between an
-           'encoded-word' and surrounding text.  [Section 5,
-           paragraph (2)].  However, white space is not needed between
-           the initial "(" that begins the 'comment', and the
-           'encoded-word'.
-
-
-   (=?ISO-8859-1?Q?a?= =?ISO-8859-1?Q?b?=)     (ab)
-
-           White space between adjacent 'encoded-word's is not
-           displayed.
-
-   (=?ISO-8859-1?Q?a?=  =?ISO-8859-1?Q?b?=)    (ab)
-
-        Even multiple SPACEs between 'encoded-word's are ignored
-        for the purpose of display.
-
-   (=?ISO-8859-1?Q?a?=                         (ab)
-       =?ISO-8859-1?Q?b?=)
-
-           Any amount of linear-space-white between 'encoded-word's,
-           even if it includes a CRLF followed by one or more SPACEs,
-           is ignored for the purposes of display.
-
-
-
-Moore                       Standards Track                    [Page 12]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-   (=?ISO-8859-1?Q?a_b?=)                      (a b)
-
-           In order to cause a SPACE to be displayed within a portion
-           of encoded text, the SPACE MUST be encoded as part of the
-           'encoded-word'.
-
-   (=?ISO-8859-1?Q?a?= =?ISO-8859-2?Q?_b?=)    (a b)
-
-           In order to cause a SPACE to be displayed between two strings
-           of encoded text, the SPACE MAY be encoded as part of one of
-           the 'encoded-word's.
-
-9. References
-
-   [RFC 822] Crocker, D., "Standard for the Format of ARPA Internet Text
-       Messages", STD 11, RFC 822, UDEL, August 1982.
-
-   [RFC 2049] Borenstein, N., and N. Freed, "Multipurpose Internet Mail
-       Extensions (MIME) Part Five: Conformance Criteria and Examples",
-       RFC 2049, November 1996.
-
-   [RFC 2045] Borenstein, N., and N. Freed, "Multipurpose Internet Mail
-       Extensions (MIME) Part One: Format of Internet Message Bodies",
-       RFC 2045, November 1996.
-
-   [RFC 2046] Borenstein N., and N. Freed, "Multipurpose Internet Mail
-       Extensions (MIME) Part Two: Media Types", RFC 2046,
-       November 1996.
-
-   [RFC 2048] Freed, N., Klensin, J., and J. Postel, "Multipurpose
-       Internet Mail Extensions (MIME) Part Four: Registration
-       Procedures", RFC 2048, November 1996.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Moore                       Standards Track                    [Page 13]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-10. Security Considerations
-
-   Security issues are not discussed in this memo.
-
-11. Acknowledgements
-
-   The author wishes to thank Nathaniel Borenstein, Issac Chan, Lutz
-   Donnerhacke, Paul Eggert, Ned Freed, Andreas M. Kirchwitz, Olle
-   Jarnefors, Mike Rosin, Yutaka Sato, Bart Schaefer, and Kazuhiko
-   Yamamoto, for their helpful advice, insightful comments, and
-   illuminating questions in response to earlier versions of this
-   specification.
-
-12. Author's Address
-
-   Keith Moore
-   University of Tennessee
-   107 Ayres Hall
-   Knoxville TN 37996-1301
-
-   EMail: moore@cs.utk.edu
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-Moore                       Standards Track                    [Page 14]
-
-RFC 2047               Message Header Extensions           November 1996
-
-
-Appendix - changes since RFC 1522 (in no particular order)
-
-   + explicitly state that the MIME-Version is not requried to use
-     'encoded-word's.
-
-   + add explicit note that SPACEs and TABs are not allowed within
-     'encoded-word's, explaining that an 'encoded-word' must look like an
-     'atom' to an RFC822 parser.values, to be precise).
-
-   + add examples from Olle Jarnefors (thanks!) which illustrate how
-     encoded-words with adjacent linear-white-space are displayed.
-
-   + explicitly list terms defined in RFC822 and referenced in this memo
-
-   + fix transcription typos that caused one or two lines and a couple of
-     characters to disappear in the resulting text, due to nroff quirks.
-
-   + clarify that encoded-words are allowed in '*text' fields in both
-     RFC822 headers and MIME body part headers, but NOT as parameter
-     values.
-
-   + clarify the requirement to switch back to ASCII within the encoded
-     portion of an 'encoded-word', for any charset that uses code switching
-     sequences.
-
-   + add a note about 'encoded-word's being delimited by "(" and ")"
-     within a comment, but not in a *text (how bizarre!).
-
-   + fix the Andre Pirard example to get rid of the trailing "_" after
-     the =E9.  (no longer needed post-1342).
-
-   + clarification: an 'encoded-word' may appear immediately following
-     the initial "(" or immediately before the final ")" that delimits a
-     comment, not just adjacent to "(" and ")" *within* *ctext.
-
-   + add a note to explain that a "B" 'encoded-word' will always have a
-     multiple of 4 characters in the 'encoded-text' portion.
-
-   + add note about the "=" in the examples
-
-   + note that processing of 'encoded-word's occurs *after* parsing, and
-     some of the implications thereof.
-
-   + explicitly state that you can't expect to translate between
-     1522 and either vanilla 822 or so-called "8-bit headers".
-
-   + explicitly state that 'encoded-word's are not valid within a
-     'quoted-string'.
-
-
-
-Moore                       Standards Track                    [Page 15]
-
diff --git a/lib/pro_tacts/config.rb b/lib/pro_tacts/config.rb
index e37e755..335a12f 100644
--- a/lib/pro_tacts/config.rb
+++ b/lib/pro_tacts/config.rb
@@ -24,9 +24,10 @@ module ProTacts
     end
 
     # Whether the exchange log keeps every DAV exchange rather than the
-    # ones that went wrong, so a write that succeeded can be read back
-    # (tasks/probe.rake). Off by default because it records every sync
-    # whole. See ProTacts::ExchangeLog.
+    # ones that went wrong, so a write that succeeded can be read back —
+    # which is how a client's handling of a property gets probed at all
+    # (docs/apple-contacts.md). Off by default because it records every
+    # sync whole. See ProTacts::ExchangeLog.
     #: () -> bool
     def debug?
       value = @env.fetch("PRO_TACTS_DEBUG", nil)
diff --git a/lib/pro_tacts/proxy_auth.rb b/lib/pro_tacts/proxy_auth.rb
index 4523650..8386e5d 100644
--- a/lib/pro_tacts/proxy_auth.rb
+++ b/lib/pro_tacts/proxy_auth.rb
@@ -19,56 +19,30 @@ module ProTacts
   # Any login the proxy vouches for is accepted. Getting onto the tailnet
   # is the access control; the login only picks which address book a
   # client syncs (docs/plans/2026-09-12-per-user-books.md).
+  #
+  # The value arrives as written. Caddy copies a placeholder through
+  # verbatim, and a tailnet login is an email address, so there is no
+  # header encoding to undo — `tailscale serve` would MIME-encode a
+  # non-ASCII value (it Q-encodes what it writes), but nothing in front
+  # of this app is serve.
   module ProxyAuth
-    # One RFC 2047 encoded-word (section 2), and the whitespace after it
-    # when another follows: section 6.2 has that whitespace dropped, so a
-    # long value split across words decodes whole.
-    ENCODED_WORD = /=\?([^?\s]+)\?([QqBb])\?([^?\s]+)\?=/ #: Regexp
-    ENCODED_RUN = /#{ENCODED_WORD}(?:[ \t]+(?=#{ENCODED_WORD}))?/ #: Regexp
-    private_constant :ENCODED_WORD, :ENCODED_RUN
-
-    # Remote-User as Rack spells it in the environment.
-    ENV_KEY = "HTTP_REMOTE_USER" #: String
+    # The header the proxy writes the login to, and Rack's spelling of
+    # the same field in the environment. One pair, because three places
+    # name it: the read below, the 401 that says which header was read
+    # (Web#unauthorized), and the Sentry scrubber that keeps it out of
+    # an event (config.ru).
+    HEADER = "Remote-User" #: String
+    ENV_KEY = "HTTP_#{HEADER.upcase.tr("-", "_")}" #: String
 
-    # The login on a request, or nil for one that names nobody.
+    # The login on a request, or nil for one that names nobody. Read as
+    # UTF-8, which is the encoding a login reaches the store in
+    # (AGENTS.md's note on STRICT columns); bytes that are not text at
+    # all name nobody rather than raising at the bind, several layers
+    # down, on a group named after them.
     #: (Rack::env env) -> String?
     def self.login(env)
-      login = decode(env[ENV_KEY].to_s)&.strip
-      login unless login.nil? || login.empty?
-    end
-
-    # A header value as serve writes it: Go's mime.QEncoding over the
-    # UTF-8 value (encTailscaleHeaderValue in tailscale's
-    # ipn/ipnlocal/serve.go), which leaves an ASCII value as it is. Nil
-    # for anything that does not decode to UTF-8 — a charset other than
-    # UTF-8 included, since serve never writes one.
-    #: (String value) -> String?
-    def self.decode(value)
-      decoded = value.dup.force_encoding(Encoding::UTF_8).gsub(ENCODED_RUN) {
-        charset, encoding, text = $1.to_s, $2.to_s, $3.to_s
-        return nil unless charset.casecmp?("utf-8")
-
-        decode_word(encoding, text) or return nil
-      }
-      decoded if decoded.valid_encoding?
-    end
-
-    # Q is section 4.2, whose underscore is a space whatever else the
-    # text holds; B is section 4.1's base64, strict so a malformed word
-    # is refused rather than read as whatever part of it decodes.
-    #: (String encoding, String text) -> String?
-    def self.decode_word(encoding, text)
-      bytes =
-        if encoding.casecmp?("q")
-          text.tr("_", " ").unpack1("M")
-        else
-          text.unpack1("m0")
-        end #: String
-      bytes.force_encoding(Encoding::UTF_8)
-    rescue ArgumentError
-      # unpack1("m0")'s refusal of text that is not base64.
-      nil
+      login = env[ENV_KEY].to_s.dup.force_encoding(Encoding::UTF_8).strip
+      login if login.valid_encoding? && !login.empty?
     end
-    private_class_method :decode_word
   end
 end
diff --git a/lib/pro_tacts/vcard.rb b/lib/pro_tacts/vcard.rb
index 673d471..ebc837a 100644
--- a/lib/pro_tacts/vcard.rb
+++ b/lib/pro_tacts/vcard.rb
@@ -6,17 +6,11 @@ module ProTacts
   # Parser::Line per logical line — and is the enumeration the
   # questions that move bytes are asked over; #properties folds the
   # same walk down to structure, and #insert puts lines back in. vCard
-  # 3.0 (RFC 2426): escape and fold, the writer's half, are here;
+  # 3.0 (RFC 2426): escape, the writer's half, is here;
   # Parser owns the reading half, down to the line split and the
   # values it comes back as. The walk is lazy for the same reason
   # Contact's parse is: the byte-moving paths never read structure.
   class VCard
-    # Folded lines must not exceed 75 octets, excluding the line break
-    # (RFC 2426 section 2.6). The octet count, not character count, is
-    # what matters: a continuation must never split a multibyte
-    # character.
-    LINE_LIMIT = 75 #: Integer
-
     TEXT_ESCAPES = {
       "\\" => "\\\\",
       ";" => "\\;",
@@ -89,27 +83,6 @@ module ProTacts
       "#{prefix}#{property.name}#{parameters.join}:"
     end
 
-    # Folds a logical line into physical lines of at most LINE_LIMIT
-    # octets, each continuation starting with a single space (RFC 2426
-    # section 2.6). The walk is character-wise so a multibyte character
-    # is never split mid-sequence.
-    #: (String line) -> String
-    def self.fold(line)
-      return line if line.bytesize <= LINE_LIMIT
-
-      folded = +""
-      width = 0
-      line.chars.each do |char|
-        if width + char.bytesize > LINE_LIMIT
-          folded << "\r\n "
-          width = 1
-        end
-        folded << char
-        width += char.bytesize
-      end
-      folded
-    end
-
     # A card is made of UTF-8 text, and refuses to be made of anything
     # else: bytes that are not valid UTF-8 raise here, at the boundary,
     # rather than leaking an ArgumentError out of whatever regex first
diff --git a/lib/pro_tacts/vcard/parser.rb b/lib/pro_tacts/vcard/parser.rb
index dbb339b..9c75bf1 100644
--- a/lib/pro_tacts/vcard/parser.rb
+++ b/lib/pro_tacts/vcard/parser.rb
@@ -196,11 +196,13 @@ module ProTacts
         logical_lines(card).map { |logical_line| line_of(logical_line) }
       end
 
-      # VCard.fold's inverse, and the first thing a read does: RFC 2426
-      # section 2.6 has a content line unfolded before it is read. It
-      # runs over the whole line rather than token by token because the
-      # RFC puts no constraint on where a fold lands — mid-token and
-      # mid-escape included — so there is no boundary to do it at.
+      # The first thing a read does: RFC 2426 section 2.6 has a content
+      # line unfolded before it is read. It stands alone, with no folder
+      # opposite it — a served card is stored bytes going out untouched,
+      # so nothing here has a logical line to fold. It runs over the
+      # whole line rather than token by token because the RFC puts no
+      # constraint on where a fold lands — mid-token and mid-escape
+      # included — so there is no boundary to do it at.
       #
       # The recorded macOS session folds nothing: it sent a 443-octet
       # X-ADDRESSING-GRAMMAR line and an 81-octet ADR unbroken, both
diff --git a/lib/pro_tacts/web.rb b/lib/pro_tacts/web.rb
index d7b7a06..e72ff6a 100644
--- a/lib/pro_tacts/web.rb
+++ b/lib/pro_tacts/web.rb
@@ -119,7 +119,7 @@ module ProTacts
       response["WWW-Authenticate"] = "Proxy-Identity"
       response["Content-Type"] = "text/plain"
       response.write(
-        "Unauthorized: no login on the Remote-User header.\n" \
+        "Unauthorized: no login on the #{ProxyAuth::HEADER} header.\n" \
         "The proxy in front of this app writes that header.\n"
       )
       r.halt
diff --git a/lib/pro_tacts/web/dav.rb b/lib/pro_tacts/web/dav.rb
index 09e5ba4..74fd8ba 100644
--- a/lib/pro_tacts/web/dav.rb
+++ b/lib/pro_tacts/web/dav.rb
@@ -87,7 +87,7 @@ module ProTacts
               # granting some of them. macOS Contacts asks for this
               # property on every poll and attempts no write without it,
               # so claiming them is what makes the client send writes at
-              # all — the PUT they prompted was what log/unhandled
+              # all — the PUT they prompted is what the exchange log
               # captured them for. DAV:write covers PUT and PROPPATCH
               # (RFC 3744 section 3.2), and PUT is the one of the pair
               # answered; DAV:unbind is removing a member from the
diff --git a/monica/README.md b/monica/README.md
deleted file mode 100644
index be534dc..0000000
--- a/monica/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-username: foo@example.com
-password: ajv-qua1vxu*KHN@twa
-personal_access_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiZmIzOGI2N2RmZjczMjczMjBlMzM4YWUwOThjZjNjYTRhNWMyOTYwZDc3N2E0NzhhYTUyMDhlOTk3NGY1YWM3MmE3NWE0MzFmNjY1NGIxNzIiLCJpYXQiOjE3Njg0Mjc4NTEuNDQ0ODM2LCJuYmYiOjE3Njg0Mjc4NTEuNDQ0ODM3LCJleHAiOjE3OTk5NjM4NTEuNDM0NTg4LCJzdWIiOiIxIiwic2NvcGVzIjpbXX0.K75qTQzUpSkxj0NIYNlhLMm2bNEhYfRGZk-MUNwvniSGaQNHGp-OUh-Zh-DXXjyi6IKFiNRRfSGzS3fOYZTAkX1Pu0TFwqmgKAjXJhjl-2eBIUxYh1Ept_di8rAt-oxl9sCFiEMxrAq-oAJaUQljEYnSB6gdk7F0kU_BCD0wHd3SrSvFBAvqWsNwRlsERvEB3C_cw3y66m-72NWv0dD61x3kPrdlkpZOu6GbFICkHXxR0jWs5e6USdR_eKe81Zn3of1IRilbBCZXSjW616bxkBfF6QMaYTkbayIu1x5weIy3rzYXGXSTrI2JTJVr1iNRKtc6Zy34DHfwDesmLHMTNPHxqB5eQRTBKpSAstI8qKDPJSNcNH8QXIMizfXmJtnXCDs1RQp6T0aGnVSLoWnQxPaAdCRi7mkJ-UeYPTSlKpraGfNs3ewRCHJVNRfx5g6wK6wANY7iC-Edxdxkm9AFoOfVB62eLeXG0ss0X9fnNW7z3BvEfedyzelUxipJ5vfnCWQSJGiAfaZI5bY94TF8TfFsshvMoS5UqpwGIUJRFTjYC_-28Hn4Dn1IrmKgvMlBWAE6EowTPPsnZc0CylKJCY8WfuNjAqXgoWEj1MULiWWa6fyLOaqWdRCNOjO-6GWzhc0xCHO4SBru6yu7JKIW4y1BMb5m28wcuFs_Fm2gx4U
diff --git a/tasks/probe.rake b/tasks/probe.rake
deleted file mode 100644
index 6d641d0..0000000
--- a/tasks/probe.rake
+++ /dev/null
@@ -1,287 +0,0 @@
-# Probes for client behavior this server cannot reason its way to. A
-# probe is a card built to be edited by a real client and read back off
-# the wire, because what Apple does with a property is empirical and
-# every guess about it has been wrong at least once
-# (docs/apple-contacts.md).
-#
-# Seeded over HTTP rather than into the database, so the card travels
-# the same route a client's would and the dev server needs no
-# cooperation beyond running.
-
-namespace :probe do
-  # Puts a probe card where a client will sync it. The id is the UID:
-  # this server's cards are named by it (see ProTacts::Contact), and a
-  # PUT whose body disagrees is refused.
-  def seed(id, card)
-    require "net/http"
-    require "uri"
-
-    # rackup's default, which is what `rake dev` starts.
-    url = URI.parse(ENV.fetch("PROBE_URL", "http://localhost:9292"))
-    url.path = "/dav/addressbook/#{id}.vcf"
-
-    request = Net::HTTP::Put.new(url)
-    request["Content-Type"] = "text/vcard"
-    # Serve strips these headers from incoming requests and sets them
-    # from the tailnet identity, so supplying them is only possible — and
-    # only meaningful — against a dev server on localhost
-    # (ProTacts::TailscaleAuth).
-    request["Tailscale-User-Login"] = ENV.fetch("PROBE_LOGIN", "probe@example.com")
-    request["Tailscale-User-Name"] = ENV.fetch("PROBE_NAME", "Probe")
-    request.body = card
-
-    response = Net::HTTP.start(url.hostname, url.port) { it.request(request) }
-    abort "#{url} answered #{response.code}: #{response.body}" unless response.code.start_with?("2")
-    response.code
-  end
-
-  # The shared half of every probe's instructions: a successful PUT did
-  # not go wrong, so the exchange log keeps it only with PRO_TACTS_DEBUG
-  # set. `edit` is what to change in the client: a probe that reads
-  # what the rewrite does to the rest of the card does not care, and
-  # one whose experiment is the edit itself says which to make.
-  def reading_the_result(id, name, edit: "any field, the edit itself does not matter")
-    <<~STEPS
-      1. The server needs PRO_TACTS_DEBUG=1 for the write to be logged —
-         without it the exchange log keeps only exchanges that went
-         wrong. Restart with `PRO_TACTS_DEBUG=1 rake dev` if it is not
-         set.
-      2. Resync the client and let a write through, editing "#{name}":
-         #{edit}.
-      3. The PUT body lands in log/dev.log after a `>>` line, under
-         #{id}.vcf.
-    STEPS
-  end
-
-  # The annotation forms under test, one card so a single edit answers
-  # all five. Each is independent: whatever the client keeps, drops, or
-  # renumbers, it does to each of these on its own terms.
-  #
-  #   X-PT-GROUP            a standalone unmodeled property — the
-  #                         control, and the only form with prior
-  #                         evidence, since macOS writes X- properties
-  #                         of its own (X-ADDRESSING-GRAMMAR)
-  #   ADR;X-PT-GROUP=       an unknown parameter on a property whose
-  #                         parameter section macOS rebuilds from its
-  #                         own model — the form the plan assumed and
-  #                         the one most likely to be lost
-  #   NOTE;X-PT-GROUP=      the same on a property macOS has not been
-  #                         seen to re-parameterize
-  #   item9.X-PT-GROUP      a companion line in a property group with
-  #                         no label the client models
-  #   item8.X-PT-GROUP      a companion beside an X-ABLabel that it
-  #                         does, in case the known line is what
-  #                         anchors the group through a renumber
-  PROBE_ID = "probe-annotations"
-  PROBE_CARD = <<~CARD.gsub("\n", "\r\n")
-    BEGIN:VCARD
-    VERSION:3.0
-    N:Boole;Probe;;;
-    FN:Probe Boole
-    X-PT-GROUP:kxsv
-    ADR;TYPE=home;X-PT-GROUP=kxsv:;;7 Calculus Close;London;England;NW1 1AB;United Kingdom
-    NOTE;X-PT-GROUP=kxsv:Gate code 1854.
-    item9.ADR;TYPE=work:;;1 Long Road;London;England;NW1 1AB;United Kingdom
-    item9.X-PT-GROUP:kxsv
-    item8.TEL;TYPE=cell:+44 20 5555 0100
-    item8.X-ABLabel:household
-    item8.X-PT-GROUP:kxsv
-    UID:#{PROBE_ID}
-    END:VCARD
-  CARD
-
-  # The follow-up to that one, which answered the companion question
-  # with numbering nothing here chose deliberately: item8 and item9,
-  # picked to dodge collisions, both got renumbered or stripped, so
-  # "the marker does not follow its group" and "the numbering confused
-  # the client" were not told apart.
-  #
-  # This card separates them. Two groups, both spelled the way a client
-  # spells its own — dense, in document order, each anchored by an
-  # X-ABLabel macOS models — with one gap:
-  #
-  #   item1  needs no renumbering. If its marker comes back at item1,
-  #          an untouched group keeps its companion.
-  #   item3  has to move to item2 for the numbering to be dense again.
-  #          Its marker either follows the group it belongs to, or it
-  #          stays at item3 while the group moves out from under it.
-  #
-  # The second is the whole experiment: a marker that stays put is a
-  # binding the client never maintained, and one that follows is a
-  # binding provenance could rest on. The first is the control that
-  # says the card was well-formed enough to be kept at all.
-  RENUMBER_ID = "probe-renumber"
-  RENUMBER_CARD = <<~CARD.gsub("\n", "\r\n")
-    BEGIN:VCARD
-    VERSION:3.0
-    N:Boole;Renumber;;;
-    FN:Probe Renumber
-    item1.TEL;TYPE=cell:+44 20 5555 0100
-    item1.X-ABLabel:household
-    item1.X-PT-GROUP:kxsv
-    item3.EMAIL;TYPE=internet:probe@example.com
-    item3.X-ABLabel:household
-    item3.X-PT-GROUP:mnop
-    UID:#{RENUMBER_ID}
-    END:VCARD
-  CARD
-
-  # What a round trip does to an address's TYPE, which decides what a
-  # relabelled shared address means (Store#kept_types?). A group's line
-  # is subtracted from a member's submission when the value and the
-  # types both survive, so a type the client rewrites unasked reads as
-  # an edit — and once edits propagate, one member's sync would rewrite
-  # the shared address for everyone.
-  #
-  # Three addresses, told apart by their streets, since values survive
-  # exactly:
-  #
-  #   TYPE=home   a type the client models, and the control: known to
-  #               come back HOME with pref added
-  #   TYPE=dom    a type RFC 2426 section 3.2.1 lists and Contacts has
-  #               no field for — the one that decides it
-  #   (none)      the RFC's own default, TYPE=intl,postal,parcel,work,
-  #               against the annotation probe's bare ADR coming back
-  #               typed WORK
-  TYPES_ID = "probe-types"
-  TYPES_CARD = <<~CARD.gsub("\n", "\r\n")
-    BEGIN:VCARD
-    VERSION:3.0
-    N:Boole;Types;;;
-    FN:Probe Types
-    ADR;TYPE=home:;;1 Modeled Way;London;England;NW1 1AB;United Kingdom
-    ADR;TYPE=dom:;;2 Unmodeled Way;London;England;NW1 1AB;United Kingdom
-    ADR:;;3 Untyped Way;London;England;NW1 1AB;United Kingdom
-    UID:#{TYPES_ID}
-    END:VCARD
-  CARD
-
-  desc "Seed the address type probe: what survives a relabel, and what the client relabels unasked"
-  task :types do
-    code = seed(TYPES_ID, TYPES_CARD)
-
-    puts <<~NEXT
-      Seeded #{TYPES_ID} (#{code}).
-
-      #{reading_the_result(TYPES_ID, "Probe Types", edit: %(relabel "1 Modeled Way" from Home to Work, and leave the other two addresses alone)).chomp}
-      4. Read the three addresses that come back:
-         - 1 Modeled Way: did the relabel arrive as type=WORK, so a
-           member's relabel is a thing this server can see at all?
-         - 2 Unmodeled Way: did TYPE=dom survive on an address nobody
-           touched, or did the client replace it the way it corrects
-           TEL;TYPE=mobile to CELL? A replacement means an untouched
-           shared address reads as edited, which is what Store#kept_types?
-           cannot carry into a propagating write.
-         - 3 Untyped Way: which types did the client write where the
-           card carried none?
-      5. Record what happened in docs/apple-contacts.md, under "The
-         client rewrites every card it touches".
-      6. Then relabel one address to a custom label and read it again:
-         a custom label is an itemN.ADR with an X-ABLabel beside it,
-         two lines where the group lends one.
-    NEXT
-  end
-
-  desc "Seed the annotation probe card into a running dev server (PROBE_URL to override)"
-  task :annotations do
-    code = seed(PROBE_ID, PROBE_CARD)
-
-    puts <<~NEXT
-      Seeded #{PROBE_ID} (#{code}).
-
-      #{reading_the_result(PROBE_ID, "Probe Boole").chomp}
-      4. Compare it with the card in tasks/probe.rake: which of the five
-         forms came back, whether itemN was renumbered, and whether a
-         companion stayed with the line it was grouped with.
-      5. Record what happened in docs/apple-contacts.md. Repeat on iOS,
-         which numbers item groups differently (see
-         test/fixtures/ios-exchange/05-put-contact-edit/request).
-    NEXT
-  end
-
-  desc "Seed the renumbering probe: does a companion follow its group?"
-  task :renumber do
-    code = seed(RENUMBER_ID, RENUMBER_CARD)
-
-    puts <<~NEXT
-      Seeded #{RENUMBER_ID} (#{code}).
-
-      #{reading_the_result(RENUMBER_ID, "Probe Renumber").chomp}
-      4. Read the two groups that come back:
-         - item1.TEL and its X-ABLabel should not have to move. Did
-           item1.X-PT-GROUP come back beside them?
-         - item3.EMAIL and its X-ABLabel should land on item2. Did
-           item3.X-PT-GROUP follow them there, or stay at item3?
-         The second answers whether a companion is a binding the client
-         maintains or two numbers that happened to agree.
-      5. Record what happened in docs/apple-contacts.md, under "An
-         annotation survives only on its own line".
-    NEXT
-  end
-
-  # What a composed member looks like to a client: its own NOTE and the
-  # one its group lends, two lines where RFC 2426 section 3.6.2 gives a
-  # note "a single text value" and RFC 6350 section 6.7.2 allows any
-  # number of them. Told apart by their text, since values survive.
-  NOTES_ID = "probe-notes"
-  NOTES_CARD = <<~CARD.gsub("\n", "\r\n")
-    BEGIN:VCARD
-    VERSION:3.0
-    N:Boole;Notes;;;
-    FN:Notes Boole
-    NOTE:Own note: gate code 1854.
-    NOTE:Lent note: bins go out on Tuesday.
-    UID:#{NOTES_ID}
-    END:VCARD
-  CARD
-
-  # Whether tags can ride in the card as CATEGORIES (RFC 2426 section
-  # 3.6.1) rather than becoming server-side records. Two lines, one
-  # holding two values, because a client that keeps the property might
-  # still merge the lines or split the list.
-  CATEGORIES_ID = "probe-categories"
-  CATEGORIES_CARD = <<~CARD.gsub("\n", "\r\n")
-    BEGIN:VCARD
-    VERSION:3.0
-    N:Boole;Categories;;;
-    FN:Categories Boole
-    CATEGORIES:family,school run
-    CATEGORIES:neighbours
-    UID:#{CATEGORIES_ID}
-    END:VCARD
-  CARD
-
-  desc "Seed the two-NOTE probe: what a client shows and writes for a composed note"
-  task :notes do
-    code = seed(NOTES_ID, NOTES_CARD)
-
-    puts <<~NEXT
-      Seeded #{NOTES_ID} (#{code}).
-
-      Before editing, look at the Note field in Contacts: does it show
-      the own note, the lent note, both joined, or only one?
-
-      #{reading_the_result(NOTES_ID, "Notes Boole", edit: "any field other than the note").chomp}
-      4. Read the NOTE lines that come back: both as sent, one joined
-         line, or one dropped — and which.
-      5. Record what happened in docs/apple-contacts.md.
-    NEXT
-  end
-
-  desc "Seed the CATEGORIES probe: does a tag survive a client round trip?"
-  task :categories do
-    code = seed(CATEGORIES_ID, CATEGORIES_CARD)
-
-    puts <<~NEXT
-      Seeded #{CATEGORIES_ID} (#{code}).
-
-      Before editing, look for the categories anywhere in Contacts —
-      the card, a smart group, search for "neighbours".
-
-      #{reading_the_result(CATEGORIES_ID, "Categories Boole").chomp}
-      4. Read the CATEGORIES lines that come back: both as sent, merged
-         into one, split one value per line, or gone.
-      5. Record what happened in docs/apple-contacts.md.
-    NEXT
-  end
-end
diff --git a/test/fixtures/macos-exchange/README.md b/test/fixtures/macos-exchange/README.md
index 5b5cbca..8b93211 100644
--- a/test/fixtures/macos-exchange/README.md
+++ b/test/fixtures/macos-exchange/README.md
@@ -1,8 +1,8 @@
 # macOS exchange fixtures
 
 Request-level recordings of what macOS Contacts (26.5.1,
-AddressBookCore/2732.600.11) has sent to pro-tacts, promoted from
-`log/unhandled` as each session landed. Steps 01–09 are the read-only
+AddressBookCore/2732.600.11) has sent to pro-tacts, promoted from the
+exchange log as each session landed. Steps 01–09 are the read-only
 exchange of 2026-08-14, the day the one-card milestone landed (see
 `docs/plans/2026-08-12-one-card-on-macos.md`). Steps 10–11 are the
 writes the client began sending once the collection advertised write
diff --git a/test/photo_card.rb b/test/photo_card.rb
index 27696bc..7a87d25 100644
--- a/test/photo_card.rb
+++ b/test/photo_card.rb
@@ -3,7 +3,7 @@ require "base64"
 require "pro_tacts/vcard"
 
 # Builds the cards macOS Contacts sends for a contact with a picture,
-# in the exact wire shapes of the two captures in log/unhandled
+# in the exact wire shapes of the two logged exchanges
 # (put-...-1a6555e1 for the memoji, put-...-ae05fe03 for the photo).
 # The captures stay unpromoted — possibly real images, see
 # test/fixtures/macos-exchange/README.md — so the shapes live here as
@@ -62,9 +62,8 @@ module PhotoCard
 
   # The photo property as the client folds it, per the module comment:
   # parameters unbroken on one line, payload a space then 76 octets a
-  # continuation. Deliberately not VCard.fold, which honors the
-  # 75-octet limit the client ignores here — this half builds what
-  # arrives, and the writer's half is VCard's.
+  # continuation — past the 75-octet limit of RFC 2426 section 2.6, which
+  # is the point. This builds what arrives, not what the spec asks for.
   #: (Array[String] parameters, String image_bytes) -> String
   def self.property(parameters, image_bytes)
     base64 = Base64.strict_encode64(image_bytes)
diff --git a/test/pro_tacts/test_proxy_auth.rb b/test/pro_tacts/test_proxy_auth.rb
index b8efa06..0d39e26 100644
--- a/test/pro_tacts/test_proxy_auth.rb
+++ b/test/pro_tacts/test_proxy_auth.rb
@@ -31,33 +31,13 @@ class ProxyAuthTest < Minitest::Test
     assert_nil login(sent_as: "HTTP_TAILSCALE_USER_LOGIN")
   end
 
-  # What Go's mime.QEncoding writes for a non-ASCII value, which is how
-  # serve sends one.
-  def test_a_q_encoded_login_is_decoded
-    assert_equal "zoë@example.com", login("=?utf-8?q?zo=C3=AB@example.com?=")
+  # The proxy copies the value through, so a non-ASCII one arrives as the
+  # UTF-8 it was written in rather than MIME-encoded.
+  def test_a_non_ascii_login_is_read_as_utf8
+    assert_equal "zoë@example.com", login((+"zoë@example.com").force_encoding(Encoding::BINARY))
   end
 
-  def test_whitespace_between_encoded_words_is_dropped
-    assert_equal "Zoë Chen", login("=?utf-8?q?Zo=C3=AB?= =?utf-8?q?_Chen?=")
-  end
-
-  def test_whitespace_around_plain_text_is_kept
-    assert_equal "Dr Zoë Chen", login("Dr =?utf-8?q?Zo=C3=AB?= Chen")
-  end
-
-  def test_a_b_encoded_login_is_decoded
-    assert_equal "Zoë Chen", login("=?UTF-8?B?Wm/DqyBDaGVu?=")
-  end
-
-  def test_a_login_in_another_charset_is_nobody
-    assert_nil login("=?iso-8859-1?q?Zo=EB?=")
-  end
-
-  def test_a_login_that_decodes_to_invalid_utf8_is_nobody
-    assert_nil login("=?utf-8?q?Zo=EB?=")
-  end
-
-  def test_a_malformed_b_word_is_nobody
-    assert_nil login("=?utf-8?b?not*base64?=")
+  def test_a_login_that_is_not_utf8_is_nobody
+    assert_nil login((+"zo\xEB").force_encoding(Encoding::BINARY))
   end
 end
diff --git a/test/pro_tacts/test_vcard.rb b/test/pro_tacts/test_vcard.rb
index 20bde3d..0ff5c70 100644
--- a/test/pro_tacts/test_vcard.rb
+++ b/test/pro_tacts/test_vcard.rb
@@ -5,16 +5,14 @@ require "hegel"
 require "pro_tacts/vcard"
 require "pro_tacts/vcard/parser"
 
-# The writer's half of the module: nothing serves through escape and
-# fold yet, and a PUT or the web editor will.
+# The writer's half of the module: escape and header_of, which the web
+# editor's saves are rebuilt through (Admin::CardForm).
 class VCardTest < Minitest::Test
   include Hegel::Syntax::Methods
 
   ## Escaping
 
-  # RFC 2426 section 2.4.2. escape and fold are the writer's half of this
-  # module: nothing serves through them yet, and a PUT or the web editor
-  # will.
+  # RFC 2426 section 2.4.2.
   def test_the_separators_and_the_backslash_escape
     assert_equal "Smith\\, John\\; Jr.", ProTacts::VCard.escape("Smith, John; Jr.")
     assert_equal "a\\\\b", ProTacts::VCard.escape("a\\b")
@@ -26,22 +24,6 @@ class VCardTest < Minitest::Test
     assert_equal "a\\nb\\nc\\nd", ProTacts::VCard.escape("a\r\nb\rc\nd")
   end
 
-  def test_a_line_at_the_limit_is_left_alone
-    line = "NOTE:#{"x" * (ProTacts::VCard::LINE_LIMIT - 5)}"
-
-    assert_equal ProTacts::VCard::LINE_LIMIT, line.bytesize
-    assert_equal line, ProTacts::VCard.fold(line)
-  end
-
-  def test_folding_never_splits_a_character
-    folded = ProTacts::VCard.fold("NOTE:#{"\u00e9" * 60}")
-
-    folded.split("\r\n").each do |physical|
-      assert_operator physical.bytesize, :<=, ProTacts::VCard::LINE_LIMIT
-      assert physical.valid_encoding?
-    end
-  end
-
   ## Unescaping
 
   def test_unescape_reverses_escape
@@ -118,17 +100,6 @@ class VCardTest < Minitest::Test
     assert_equal 'TEL;X-FOO="a:b,c":', ProTacts::VCard.header_of(quoted)
   end
 
-  ## Property tests
-
-  def test_unfolding_reverses_folding
-    Hegel.test do |tc|
-      value = tc.draw(text(max_size: 300))
-      line = "NOTE:#{ProTacts::VCard.escape(value)}"
-
-      raise "unfold did not reverse fold" unless ProTacts::VCard::Parser.unfold(ProTacts::VCard.fold(line)) == line
-    end
-  end
-
   ## The card's bytes
 
   # Relabelling bytes is not converting them, so bytes that are not
diff --git a/test/pro_tacts/test_web.rb b/test/pro_tacts/test_web.rb
index f08e3a8..6a0dda4 100644
--- a/test/pro_tacts/test_web.rb
+++ b/test/pro_tacts/test_web.rb
@@ -1012,7 +1012,7 @@ class WebTest < Minitest::Test
       request "/dav/addressbook/", method: "PROPFIND", "HTTP_DEPTH" => "1", input: etag_only_propfind
       refute_includes last_response.body, "znorth"
 
-      header "Remote-User", "=?utf-8?q?zo=C3=AB@example.com?="
+      header "Remote-User", "zoë@example.com"
       request "/dav/addressbook/", method: "PROPFIND", "HTTP_DEPTH" => "1", input: etag_only_propfind
       assert_includes last_response.body, "aiden.vcf"
       assert_includes last_response.body, "znorth.vcf"
diff --git a/test/pro_tacts/vcard/test_parser.rb b/test/pro_tacts/vcard/test_parser.rb
index 620728a..49d30d0 100644
--- a/test/pro_tacts/vcard/test_parser.rb
+++ b/test/pro_tacts/vcard/test_parser.rb
@@ -164,7 +164,7 @@ class VCardParserTest < Minitest::Test
   ## Pictures
 
   # The two shapes a picture arrives in, built as captured
-  # (test/photo_card.rb, from log/unhandled): the parameter section on
+  # (test/photo_card.rb, from the exchange log): the parameter section on
   # one physical line however long, the base64 payload folded one
   # break below it. The parser reads both without a length ceiling
   # because the line split comes before the token scan — nothing here
@@ -332,10 +332,34 @@ class VCardParserTest < Minitest::Test
     text.gsub(/\r\n|\r/, "\n")
   end
 
+  # Folded input for #unfold to undo, to RFC 2426 section 2.6: physical
+  # lines of at most 75 octets, each continuation a single space. The
+  # walk is character-wise so a multibyte character is never split. It
+  # lives here rather than in VCard because nothing this server writes
+  # folds — a served card is stored bytes going out untouched — so the
+  # only caller a folder has ever had is this test.
+  LINE_LIMIT = 75
+
+  def fold(line)
+    return line if line.bytesize <= LINE_LIMIT
+
+    folded = +""
+    width = 0
+    line.each_char do |char|
+      if width + char.bytesize > LINE_LIMIT
+        folded << "\r\n "
+        width = 1
+      end
+      folded << char
+      width += char.bytesize
+    end
+    folded
+  end
+
   def test_an_escaped_and_folded_value_parses_back
     Hegel.test do |tc|
       value = tc.draw(text(max_size: 300))
-      line = ProTacts::VCard.fold("NOTE:#{ProTacts::VCard.escape(value)}")
+      line = fold("NOTE:#{ProTacts::VCard.escape(value)}")
 
       parsed = properties("#{line}\r\n")
       raise "expected one property, got #{parsed.length}" unless parsed.length == 1