From 3303c7855cb22a6bb4f36aa1a17707187320f5d2 Mon Sep 17 00:00:00 2001 From: "Danilo M." Date: Fri, 18 Sep 2026 14:58:58 +0200 Subject: feat: add ContactStore, the vCard address-book parse Reads a vdirsyncer contacts directory of vCard 3.0 files into a QList for the completion work that follows. Pure over values, no widget and no QCompleter, so the parse is testable without a window. unfold() joins folded lines before any field is looked at; parseCard() splits property from value on the first colon outside a quoted parameter, unescapes FN, and yields one contact per EMAIL line; loadDirectory() walks recursively, skips unreadable or addressless cards, de-duplicates on the address case-insensitively, and sorts by name then address. N, PHOTO, ADR and TEL are deliberately not used. 23 new tests. No user-facing strings, so no tr() change. --- src/contactstore.h | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/contactstore.h (limited to 'src/contactstore.h') diff --git a/src/contactstore.h b/src/contactstore.h new file mode 100644 index 0000000..307e87b --- /dev/null +++ b/src/contactstore.h @@ -0,0 +1,79 @@ +/* + * qtmaildir - a Qt6 mail client for notmuch-indexed Maildirs + * Copyright (C) 2026 Danilo M. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#pragma once + +#include +#include + +/// One completion candidate from the address book. +/// +/// Deliberately only two fields. PHOTO, ADR and TEL are read past and dropped: +/// the user was asked and did not want a second use for the store, so anything +/// the completion cannot use is not carried. +struct Contact +{ + /// The card's FN, already unescaped. Empty is valid: a card with an + /// address but no name completes on the address alone. + QString name; + + /// The value of one EMAIL line. The identity the store de-duplicates on. + QString email; +}; + +/// Reads a vdirsyncer contacts directory of vCard 3.0 files. +/// +/// A namespace of free functions over values, like SearchTerm and MimeParser, +/// so the parse is testable without a widget and without a QCompleter. The +/// caller (MainWindow) holds the returned list and hands it to both consumers. +/// +/// **No new dependency for this.** libical is installed but its vCard parser is +/// 4.0 while this store is 3.0.20, and libicalvcal reads vCalendar 1.0 rather +/// than vCard. Two fields and a directory walk are the proportionate answer. +namespace ContactStore { + +/// Joins folded lines back into one logical line. +/// +/// A vCard line may be split between any two characters by inserting CRLF and +/// one linear whitespace character (RFC 2426). Unfolding removes the newline +/// and that one whitespace character and nothing else, so a trailing space on +/// the first line survives. A fold may land mid-token or inside base64, which +/// is why this runs over the whole file before any field is looked at. +QString unfold(const QString &text); + +/// Parses one card's already-unfolded text into one Contact per EMAIL line. +/// +/// Every entry carries the card's single FN. A card with no EMAIL yields an +/// empty list; a card with no FN yields a Contact whose name is empty. The +/// property name is matched case-insensitively, and the field/value split is +/// on the first colon that is not inside a double-quoted parameter value, so +/// `EMAIL;TYPE=WORK:a@example.org` parses. +QList parseCard(const QString &unfoldedText); + +/// Walks `directory` recursively and returns every card found, de-duplicated +/// and sorted. +/// +/// Every `*.vcf` at any depth is read, because a vdir keeps one subdirectory +/// per collection. A file that cannot be read or contributes no EMAIL is +/// skipped rather than fatal: one bad card must not cost the other 116. The +/// result is de-duplicated on the address, case-insensitively, and sorted by +/// name then address. A non-existent or empty directory yields an empty list +/// and no warning, since a machine with no vdir is an ordinary machine. +QList loadDirectory(const QString &directory); + +} // namespace ContactStore -- cgit v1.2.3