#!/bin/bash
#
# Bash library and executable that manage id compliant with foopgp: OpenPGP ID.
#
# Copyright © 2025-2026 Jean-Jacques Brucker <jjbrucker@foopgp.org>
#
# SPDX-License-Identifier: LGPL-3.0-only
#
# shellcheck disable=SC2013 # To read lines rather than words
# shellcheck disable=SC2034 # variable appears unused
# shellcheck disable=SC2046 # Quote this to prevent word splitting
# shellcheck disable=SC2086 # Double quote warning
# shellcheck disable=SC2128 # expanding array without index warning


if [[ "$1" == --bash-completion ]] ; then
	BL_tmp_a=$("${BASH_SOURCE[0]}" --help | sed -nE 's:^ {2,4}([a-z0-9_]+\>).*:\1:p')
	BL_tmp_o=$(for a in $BL_tmp_a ; do
		echo "[$a]=\"$(eval echo $("$BASH_SOURCE" $a --help | sed -n 's,^  ... \(--[a-z_-]\+[^ ]*\).*,\1,p' | sed 's_<_{_ ; s_|_,_g ; s_>_}_ '))\""
	done)

	eval '_bl_pgpid_completion()
	{
		local cur coptions="--help --version"
		local a actions="'$BL_tmp_a'"
		local -A aoptions=('$BL_tmp_o')

		COMPREPLY=()
		cur=${COMP_WORDS[COMP_CWORD]}

		case ${COMP_WORDS[COMP_CWORD-1]} in
			--help|--version)
				# should exit without executing any actions, so do not complete anything
				return 0 ;;
			--frontend)
				COMPREPLY=( $(compgen -W "whiptail dialog zenity NONE" -- $cur ) )
				return 0 ;;
			--*from)
				compopt -o plusdirs
				COMPREPLY=( $(compgen -A file -- $cur) )
				return 0 ;;
			-t|--template)
				compopt -o plusdirs
				COMPREPLY=( $(compgen -A file -X "!*.svg" -- $cur) )
				return 0 ;;
			--print|--printer)
				# Path-shaped input (./ ../ ~ / or trailing .svg) → file completion ; else printer name
				if [[ "$cur" == [/~.]* ]] ; then
					compopt -o plusdirs
					COMPREPLY=( $(compgen -A file -X "!*.svg" -- $cur) )
				else
					COMPREPLY=( $(compgen -W "$(LANG= lpstat -p 2>/dev/null | sed -n '"'"'s,^printer \([^ ]*\).*,\1,p'"'"')" -- $cur) )
				fi
				return 0 ;;
			--homedir)
				compopt -o plusdirs
				COMPREPLY=( $(compgen -A directory -- $cur) )
				return 0 ;;
		esac
		case $cur in
			-*)
				if a=$(grep -o "\<\('${BL_tmp_a//$'\n'/\\|}'\)\>" <<< "${COMP_WORDS[@]}") ; then
					COMPREPLY=( $(compgen -W "$coptions ${aoptions[$a]}" -- $cur ) )
					return 0
				fi
				COMPREPLY=( $(compgen -W "$coptions --frontend" -- $cur ) )
				return 0 ;;
		esac
		# complete actions if none in ${COMP_WORDS[@]}
		grep -q "\<\('${BL_tmp_a//$'\n'/\\|}'\)\>" <<< "${COMP_WORDS[@]}" || COMPREPLY=( $(compgen -W "$actions" -- $cur ) )
		return 0
	}'
	unset BL_tmp_a BL_tmp_o
	complete -F _bl_pgpid_completion "$(basename "$BASH_SOURCE")" "$BASH_SOURCE"
	return 0
fi

# If sourcing while _bl_pgpid_parseoptions is already set, execute the function and return without reloading rest of file.
if [[ "$BASH_SOURCE" != "$0" ]] && [[ "$(type -t _bl_pgpid_parseoptions)" == function ]] ; then
	_bl_pgpid_parseoptions "$@"
	return $?
fi

### Constants ###

BL_PGPID_NAME="$(basename "$(readlink -f "$BASH_SOURCE")" )"
BL_PGPID_VERSION="0.4.2-1"
BL_PGPID_FUNCTIONS=( $(sed -n 's,^\(bl_[^( ]*\) *().*,\1,p' "$BASH_SOURCE") )
readonly BL_PGPID_NAME BL_PGPID_VERSION BL_PGPID_FUNCTIONS

# Our Date, Time and geo coordinate — regular expressions
declare -r BL_PGPID_DATE_REGEX="(18|19|20)[0-9]{2}(0[1-9]|1[0,1,2])(0[1-9]|[12][0-9]|3[01])"
declare -r BL_PGPID_TIME_REGEX="[01-][0-9]{11}\.[0-9]{3}"				# From 'Thu Feb 15 14:22:42 LMT -1199' to 'Wed Nov 16 10:46:39 CET 5138'
declare -r BL_PGPID_CO_REGEX="e[_-][0-9]{2}\.[0-9]{2}[_-][01][0-9]{2}\.[0-9]{2}"

# Entity ID (EID) contents — regular expressions
# Note: US-ASCII into brackets is th best way to NOT manage LC_COLLATE(="C.UTF-8") - cf. https://unix.stackexchange.com/questions/645054/how-do-bash-and-zsh-handle-collation-in-patterns-and-regexes
declare -r BL_PGPID_U4H_REGEX="[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0-9_-]{22}"
declare -r BL_PGPID_U4_REGEX="(${BL_PGPID_U4H_REGEX})(${BL_PGPID_CO_REGEX})"		# Birth of "human" (must have a name, should have a soul)
declare -r BL_PGPID_N2_REGEX="${BL_PGPID_DATE_REGEX}${BL_PGPID_CO_REGEX}"		# Deprecated, don't exist anymore (should soon totally disapear)
declare -r BL_PGPID_U5_REGEX="(${BL_PGPID_TIME_REGEX})(${BL_PGPID_CO_REGEX})"		# Apparition of anything (with or without any ghost in the shell)
declare -r BL_PGPID_UX_REGEX="(${BL_PGPID_U4_REGEX}|${BL_PGPID_U5_REGEX})"		# Ombrella for all known UID contents

# Full Entity ID (EID) — regular expressions
declare -r BL_PGPID_EID_STANDARD_REGEX="((u4)(${BL_PGPID_U4_REGEX})|(u5)(${BL_PGPID_U5_REGEX}))"
declare -r BL_PGPID_EID_DEPRECATED_REGEX="u(did)?((4|5)([=:]|.x3a)(${BL_PGPID_U4_REGEX}|${BL_PGPID_U5_REGEX}))"
declare -r BL_PGPID_EID_REGEX="(${BL_PGPID_EID_STANDARD_REGEX}|${BL_PGPID_EID_DEPRECATED_REGEX})"	# Ombrella for all known EID contents

declare -r BL_PGPID_XUID_MIN=$((1<<18))
declare -r BL_PGPID_XUID_MAX=$(((1<<31)-2))

# Default sticker/stamp SVG template (85 × 25 mm, European business-card width).
# Embedded inline so the default `bl-pgpid print` works without locating a file on disk.
# A different template (typically a 85×55 mm business card) can be supplied via --template.
# Placeholders are envsubst names: ${NAME} ${U4} ${FPR_LINE1} ${FPR_LINE2} ${EMAIL} ${QR_HREF}.
declare -r BL_PGPID_STICKER_TEMPLATE='<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink"
     version="1.1"
     width="85mm" height="25mm"
     viewBox="0 0 85 25">
  <defs>
    <style type="text/css"><![CDATA[
      .card    { fill: #ffefea; stroke: #90462f; stroke-width: 0.35; }
      .accent  { fill: #ff8012; }
      .name    { font: italic bold 4.2px '\''DejaVu Sans'\'', sans-serif; fill: #1a1838; text-anchor: middle; }
      .email   { font: italic bold 3.4px '\''DejaVu Sans'\'', sans-serif; fill: #1a1838; text-anchor: middle; }
      .ident   { font: 2.4px '\''DejaVu Sans Mono'\'', monospace; fill: #ff8012; text-anchor: middle; }
      .fpr     { font: bold 2.4px '\''DejaVu Sans Mono'\'', monospace; fill: #080808; letter-spacing: 0.2px; text-anchor: middle; }
    ]]></style>
  </defs>

  <!-- Card background + top/bottom orange strips -->
  <rect class="card"   x="0.5" y="0.5"  width="84" height="24"/>
  <rect class="accent" x="0.5" y="0.5"  width="84" height="1.6" opacity="0.85"/>
  <rect class="accent" x="0.5" y="22.9" width="84" height="1.6" opacity="0.85"/>

  <!-- QR (data URI) — 20.8 × 20.8 mm, white quiet zone touches both orange strips -->
  <image x="62.2" y="2.1" width="20.8" height="20.8" xlink:href="${QR_HREF}"/>

  <!-- Text block (left of the QR, centered in [0..62.2]) -->
  <text class="name"  x="31.1" y="6.5">${NAME}</text>
  <text class="ident" x="31.1" y="10.5">${U4}</text>
  <text class="fpr"   x="31.1" y="14.0">${FPR_LINE1}</text>
  <text class="fpr"   x="31.1" y="17.0">${FPR_LINE2}</text>
  <text class="email" x="31.1" y="21.5">${EMAIL}</text>
</svg>
'

declare -Ar BL_PGPID_COORDINATES=(
[ABW]="e_12.52-069.98 Aruba"
[AFG]="e_33.84_066.00 افغانستان"
[AGO]="e-12.29_017.54 Angola"
[AIA]="e_18.22-063.06 Anguilla"
[ALB]="e_41.14_020.05 Shqipëria"
[ALA]="e_60.21_019.95 Åland"
[AND]="e_42.54_001.56 Andorra"
[ARE]="e_23.91_054.30 الإمارات العربية المتحدة"
[ARG]="e-35.38-065.18 Argentina"
[ARM]="e_40.29_044.93 Հայաստան"
[ASM]="e-14.30-170.72 Amerika Sāmoa"
[ATA]="e-80.51_019.92 Antarctica"
[ATF]="e-49.25_069.23 Terres australes et antarctiques françaises"
[ATG]="e_17.28-061.79 Antigua and Barbuda"
[AUS]="e-25.73_134.49 Australia"
[AUT]="e_47.59_014.13 Österreich"
[AZE]="e_40.29_047.55 Azərbaycan"
[BDI]="e-03.36_029.88 Uburundi/Burundi"
[BEL]="e_50.64_004.64 België/Belgique/Belgien"
[BEN]="e_09.64_002.33 Bénin"
[BFA]="e_12.27-001.75 Burkina Faso"
[BGD]="e_23.87_090.24 বাংলাদেশ"
[BGR]="e_42.77_025.22 България"
[BHR]="e_26.04_050.54 البحرين"
[CAN]="e_61.36-098.31 Canada"
[BHS]="e_24.29-076.63 Bahamas"
[BIH]="e_44.17_017.77 Bosna i Hercegovina/Босна и Херцеговина"
[BLM]="e_17.90-062.84 Saint-Barthélemy"
[BLR]="e_53.53_028.03 Беларусь"
[BLZ]="e_17.20-088.71 Belize"
[BMU]="e_32.31-064.75 Bermuda"
[BOL]="e-16.71-064.69 Bolivia/Buliwya/Wuliwya"
[BRA]="e-10.79-053.10 Brasil"
[BRB]="e_13.18-059.56 Barbados"
[BRN]="e_04.52_114.72 Brunei Darussalam"
[BTN]="e_27.41_090.40 འབྲུག་ཡུལ"
[BWA]="e-22.18_023.80 Botswana"
[CAF]="e_06.57_020.47 Ködörösêse tî Bêafrîka/République centrafricaine"
[CHE]="e_46.80_008.21 Schweiz/Suisse/Svizzera/Svizra"
[CHL]="e-37.73-071.38 Chile"
[CHN]="e_36.56_103.82 中国"
[CIV]="e_07.63-005.57 Côte d'Ivoire"
[CMR]="e_05.69_012.74 Cameroun/Cameroon"
[COD]="e-02.88_023.64 République démocratique du Congo"
[COG]="e-00.84_015.22 République du Congo"
[COK]="e-21.22-159.79 Kūki ʻĀirani/Cook Islands"
[COL]="e_03.91-073.08 Colombia"
[COM]="e-11.88_043.68 جزر القمر/Komori/Comores"
[CPV]="e_15.96-023.96 Cabo Verde"
[CRI]="e_09.98-084.19 Costa Rica"
[CUB]="e_21.62-079.02 Cuba"
[CUW]="e_12.20-068.97 Curaçao"
[CYM]="e_19.43-080.91 Cayman Islands"
[CYP]="e_34.92_033.01 Κύπρος/Kıbrıs"
[CZE]="e_49.73_015.31 Česko"
[DEU]="e_51.11_010.39 Deutschland"
[DJI]="e_11.75_042.56 جيبوتي/Djibouti"
[DMA]="e_15.44-061.36 Dominica"
[DNK]="e_55.98_010.03 Danmark"
[DOM]="e_18.89-070.51 República Dominicana"
[DZA]="e_28.16_002.62 الجزائر"
[ECU]="e-01.42-078.75 Ecuador"
[EGY]="e_26.50_029.86 مصر"
[ERI]="e_15.36_038.85 ኤርትራ/إرتريا/Ertra"
[ESP]="e_40.24-003.65 España"
[EST]="e_58.67_025.54 Eesti"
[ETH]="e_08.62_039.60 ኢትዮጵያ"
[FIN]="e_64.50_026.27 Suomi/Finland"
[FJI]="e-17.43_165.45 Viti/Fiji"
[FRA]="e_42.17-002.76 France"
[FRO]="e_62.05-006.88 Føroyar"
[FSM]="e_07.45_153.24 Federated States of Micronesia"
[GAB]="e-00.59_011.79 Gabon"
[GBR]="e_54.12-002.87 United Kingdom"
[GEO]="e_42.17_043.51 საქართველო"
[GGY]="e_49.47-002.57 Guernsey"
[GHA]="e_07.95-001.22 Ghana"
[GIN]="e_10.44-010.94 Guinée"
[GMB]="e_13.45-015.40 The Gambia"
[GNB]="e_12.05-014.95 Guiné-Bissau"
[GNQ]="e_01.71_010.34 Guinea Ecuatorial/Guinée équatoriale/Guiné Equatorial"
[GRC]="e_39.07_022.96 Ελλάδα"
[GRD]="e_12.12-061.68 Grenada"
[GRL]="e_74.71-041.34 Kalaallit Nunaat"
[GTM]="e_15.69-090.36 Guatemala"
[GUM]="e_13.44_144.77 Guåhån"
[GUY]="e_04.79-058.98 Guyana"
[HKG]="e_22.40_114.11 香港/Hong Kong"
[HMD]="e-53.09_073.52 Heard Island and McDonald Islands"
[HND]="e_14.83-086.62 Honduras"
[HRV]="e_45.08_016.40 Hrvatska"
[HTI]="e_18.94-072.69 Ayiti/Haïti"
[HUN]="e_47.16_019.40 Magyarország"
[IDN]="e-02.22_117.24 Indonesia"
[IMN]="e_54.22-004.54 Isle of Man/Ellan Vannin"
[IND]="e_22.89_079.61 भारत/India"
[IRL]="e_53.18-008.14 Éire/Ireland"
[IRN]="e_32.58_054.27 ایران"
[IRQ]="e_33.04_043.74 العراق/کوردستان"
[ISL]="e_65.00-018.57 Ísland"
[ISR]="e_31.46_035.00 ישראל/إسرائيل"
[ITA]="e_42.80_012.07 Italia"
[JAM]="e_18.16-077.31 Jamaica"
[JEY]="e_49.22-002.13 Jersey"
[JOR]="e_31.25_036.77 الأردن"
[JPN]="e_37.59_138.03 日本"
[KAZ]="e_48.16_067.29 Қазақстан/Казахстан"
[KEN]="e_00.60_037.80 Kenya"
[KGZ]="e_41.46_074.54 Кыргызстан/Кыргыз Республикасы"
[KHM]="e_12.72_104.91 កម្ពុជា"
[KIR]="e_00.86-045.61 Kiribati"
[KNA]="e_17.26-062.69 Saint Kitts and Nevis"
[KOR]="e_36.39_127.84 대한민국"
[KWT]="e_29.33_047.59 الكويت"
[LAO]="e_18.50_103.74 ປະເທດລາວ"
[LBN]="e_33.92_035.88 لبنان"
[LBR]="e_06.45-009.32 Liberia"
[LBY]="e_27.03_018.01 ليبيا"
[LCA]="e_13.89-060.97 Saint Lucia"
[LIE]="e_47.14_009.54 Liechtenstein"
[LKA]="e_07.61_080.70 ශ්‍රී ලංකා/இலங்கை"
[LSO]="e-29.58_028.23 Lesotho"
[LTU]="e_55.33_023.89 Lietuva"
[LUX]="e_49.77_006.07 Lëtzebuerg/Luxembourg/Luxemburg"
[LVA]="e_56.85_024.91 Latvija"
[MAC]="e_22.22_113.51 澳門/Macau"
[MAF]="e_18.09-063.06 Saint-Martin"
[MAR]="e_29.84-008.46 المغرب/ⵍⵎⵖⵔⵉⴱ"
[MCO]="e_43.75_007.41 Monaco"
[MDA]="e_47.19_028.46 Moldova"
[MDG]="e-19.37_046.70 Madagasikara/Madagascar"
[MDV]="e_03.73_073.46 ދިވެހިރާއްޖެ"
[MEX]="e_23.95-102.52 México"
[MHL]="e_07.00_170.34 Aolepān Aorōkin M̧ajeļ/Marshall Islands"
[MKD]="e_41.60_021.68 Северна Македонија"
[MLI]="e_17.35-003.54 Mali"
[MLT]="e_35.92_014.41 Malta"
[MMR]="e_21.19_096.49 မြန်မာ"
[MNE]="e_42.79_019.24 Crna Gora/Црна Гора"
[MNG]="e_46.83_103.05 Монгол улс"
[MNP]="e_15.83_145.62 Sankattan Siha Na Islas Mariånas"
[MOZ]="e-17.27_035.53 Moçambique"
[MRT]="e_20.26-010.35 موريتانيا"
[MSR]="e_16.74-062.19 Montserrat"
[MUS]="e-20.28_057.57 Maurice/Mauritius/Moris"
[MWI]="e-13.22_034.29 Malaŵi"
[MYS]="e_03.79_109.70 Malaysia"
[NAM]="e-22.13_017.21 Namibia"
[NCL]="e-21.30_165.68 Nouvelle-Calédonie"
[NER]="e_17.42_009.39 Niger"
[NFK]="e-29.05_167.95 Norfolk Island"
[NGA]="e_09.59_008.09 Nigeria"
[NIC]="e_12.85-085.03 Nicaragua"
[NIU]="e-19.05-169.87 Niuē/Niue"
[NLD]="e_52.10_005.28 Nederland"
[NOR]="e_68.75_015.35 Norge/Noreg"
[NPL]="e_28.25_083.92 नेपाल"
[NRU]="e-00.52_166.93 Naoero/Nauru"
[NZL]="e-41.81_171.48 Aotearoa/New Zealand"
[OMN]="e_20.61_056.09 عمان"
[PAK]="e_29.95_069.34 پاکستان"
[PAN]="e_08.52-080.12 Panamá"
[PCN]="e-24.37-128.32 Pitcairn Islands"
[PER]="e-09.15-074.38 Perú"
[PHL]="e_11.78_122.88 Pilipinas/Philippines"
[PLW]="e_07.29_134.41 Belau/Palau"
[PNG]="e-06.46_145.21 Papua Niugini/Papua New Guinea"
[POL]="e_52.13_019.39 Polska"
[PRI]="e_18.23-066.47 Puerto Rico"
[PRK]="e_40.15_127.19 조선민주주의인민공화국"
[PRT]="e_39.60-008.50 Portugal"
[PRY]="e-23.23-058.40 Paraguay/Tetã Paraguái"
[PSE]="e_31.92_035.20 فلسطين"
[PYF]="e-14.72-144.90 Polynésie française/Pōrīnetia Farāni"
[QAT]="e_25.31_051.18 قطر"
[ROU]="e_45.85_024.97 România"
[RUS]="e_61.98_096.69 Россия"
[RWA]="e-01.99_029.92 Rwanda"
[SAU]="e_24.12_044.54 المملكة العربية السعودية"
[SDN]="e_15.99_029.94 السودان"
[SSD]="e_07.31_030.25 South Sudan"
[SEN]="e_14.37-014.47 Sénégal"
[SGP]="e_01.36_103.82 Singapore/新加坡/சிங்கப்பூர்/Singapura"
[SGS]="e-54.46-036.43 South Georgia and South Sandwich Islands"
[SHN]="e-12.40-009.55 Saint Helena"
[SLB]="e-08.92_159.63 Solomon Islands"
[SLE]="e_08.56-011.79 Sierra Leone"
[SLV]="e_13.74-088.87 El Salvador"
[SMR]="e_43.94_012.46 San Marino"
[SOM]="e_04.75_045.71 Soomaaliya/الصومال"
[SPM]="e_46.92-056.30 Saint-Pierre-et-Miquelon"
[SRB]="e_44.22_020.79 Србија"
[STP]="e_00.44_006.72 São Tomé e Príncipe"
[SUR]="e_04.13-055.91 Suriname"
[SVK]="e_48.71_019.48 Slovensko"
[SVN]="e_46.12_014.80 Slovenija"
[SWE]="e_62.78_016.75 Sverige"
[SWZ]="e-26.56_031.48 Eswatini"
[SXM]="e_18.05-063.06 Sint Maarten"
[SYC]="e-04.66_055.48 Sesel/Seychelles"
[SYR]="e_35.03_038.51 سوريا"
[TCA]="e_21.83-071.97 Turks and Caicos Islands"
[TCD]="e_15.33_018.64 Tchad/تشاد"
[TGO]="e_08.53_000.96 Togo"
[THA]="e_15.12_101.00 ประเทศไทย"
[TJK]="e_38.53_071.01 Тоҷикистон"
[TKM]="e_39.12_059.37 Türkmenistan"
[TLS]="e-08.83_125.84 Timor-Leste/Timor Lorosa'e"
[TON]="e-20.43-174.81 Tonga"
[TTO]="e_10.46-061.27 Trinidad and Tobago"
[TUN]="e_34.12_009.55 تونس"
[TUR]="e_39.06_035.17 Türkiye"
[TZA]="e-06.28_034.81 Tanzania"
[UGA]="e_01.27_032.37 Uganda"
[UKR]="e_49.00_031.38 Україна"
[URY]="e-32.80-056.02 Uruguay"
[USA]="e_45.68-112.46 United States"
[UZB]="e_41.76_063.14 Oʻzbekiston"
[VAT]="e_41.90_012.43 Vaticano/Vaticanum"
[VCT]="e_13.22-061.20 Saint Vincent and the Grenadines"
[VEN]="e_07.12-066.18 Venezuela"
[VGB]="e_18.53-064.47 British Virgin Islands"
[VIR]="e_17.96-064.80 United States Virgin Islands"
[VNM]="e_16.65_106.30 Việt Nam"
[VUT]="e-16.23_167.69 Vanuatu"
[WLF]="e-13.89-177.35 Wallis-et-Futuna"
[WSM]="e-13.75-172.16 Sāmoa"
[YEM]="e_15.91_047.59 اليمن"
[ZAF]="e-29.00_025.08 South Africa/Suid-Afrika/iNingizimu Afrika"
[ZMB]="e-13.46_027.77 Zambia"
[ZWE]="e-19.00_029.85 Zimbabwe"
)

[[ "$BASH_SOURCE" == "$0" ]] && declare -r BL_PGPID_isprogram=1 || declare -r BL_PGPID_isprogram=0

### Others Globals ###

# First default keyserver MUST be hkp(s)://
[[ "${BL_PGPID_KEYSERVERS[*]}" ]] || BL_PGPID_KEYSERVERS=(
"hkps://keys.foopgp.org"
"hkps://keys.openpgp.org"
)

if ((BL_PGPID_isprogram)) ; then
	TEXTDOMAIN="bashlibs"
	TEXTDOMAINDIR="$(dirname "$(readlink -f "$BASH_SOURCE")" )/../share/locale"
	# Require Bash 5.2. - https://www.kurokatta.org/grumble/2023/11/bash-translated-strings#fnref3
	shopt -s noexpand_translation
fi

BL_PGPID_chelpmsg="
"$"Generate and manage OpenPGP ID, an OpenPGP configuration providing universal and decentralized civil status to secure your digital life (emails, git, ssh, avatar, sso, etc.).""
"$"It combines the power of OpenPGP (RFC 9580) with those of others open international standards: POSIX, ICAO 9303, ISO/IEC 7816, many others RFC.""

MAIN OPTIONS:
  -f, --frontend PROGRAM  "$"Select a frontend program"" {NONE,whiptail,dialog,zenity} - "$"Environment variable: ""BL_INTERACTIVE_FRONTEND
  -H, --homedir GNUPGHOME "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME"

### external functions ###

source "$(dirname "$BASH_SOURCE")"/bl-interactive --
source "$(dirname "$BASH_SOURCE")"/bl-security --

### internal functions ###

_bl_pgpid_parseoptions() {
	local npp=$# frontend
	for ((;$#;)) ; do
		case "$1" in
			-f|--frontend) shift; frontend="$1";; # Will be passed to bl-interactive
			-H|--homedir) shift ; export GNUPGHOME=${1:?} ;;
			-h|--help) printf "%s\n%s%s" "$BL_PGPID_usage" "$BL_PGPID_chelpmsg" "$BL_PGPID_shelpmsg" ; return 1 ;;
			-V|--version) printf "%s %s\n" "$BL_PGPID_NAME" "$BL_PGPID_VERSION" ; return 1 ;;
			--) shift ; break ;;
			-*) printf -- "$BL_PGPID_NAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$BASH_SOURCE" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done
	BL_PGPID_NOPTIONS=$((npp-$#))
	if [[ "$frontend" ]] ; then
		source "$(dirname "$BASH_SOURCE")"/bl-interactive --frontend "$frontend" --
	fi
}

_bl_icao9303_mrz_checkdigit() {
	local helpmsg="Usage: $FUNCNAME STRING [EXPECTED_RESULT]
If there is no second arg: output calculated check digit from [0-9A-Z<]* string
else return non-zero if EXPECTED_RESULT differs from calculated check digit.
"
	for ((;$#;)) ; do
		case "$1" in
			-h|--h*) echo "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) echo -e "$FUNCNAME: Error: Unrecognized option/parameters $1\n$helpmsg" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local char sum=0 weight=(7 3 1)
	for ((i=0;i<${#1};i++)) ; do
		char=${1:$i:1}
		case $char in
			[0-9]) ((sum+=char*${weight[$((i%3))]})) ;;
			[A-Z]) ((sum+=($(printf "%d" "'$char'")-55)*${weight[$((i%3))]})) ;;
		esac
	done

	if ! [[ "$2" ]] ; then
		echo $((sum%10))
	else
		[[ "$2" == $((sum%10)) ]]
		return $?
	fi
}

_bl_icao9303_mrz_analyse() {
	local helpmsg="Usage: $FUNCNAME [Options] AANAME [MRZ]
Analyse a Machine Readable Zone and fill the associative array named AANAME
If MRZ is not passed as argument, it will we read from STDIN.

OPTIONS:
  -u, --uncheck                Change some MRZ errors to warnings (size and digitchecks)
  -d, --declare                Output content of AANAME as a 'declare -A ...' statement
"

	local errlvl="Error" outdec
	for ((;$#;)) ; do
		case "$1" in
			-u|--uncheck) errlvl="Warning" ;;
			-d|--declare) outdec="." ;;
			-h|--h*) echo "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) echo -e "$FUNCNAME: Error: Unrecognized option/parameters $1\n$helpmsg" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local -n doc=${1:?}
	shift
	local mrz
	[[ "$*" ]] && mrz=$* || mrz=$(< /dev/stdin)
	mrz=${mrz//[$'\t\r\n ']}

	local ch ret=0
	if [[ ${mrz:0:1} != P ]] ; then
		# Only ICAO-9303 (ISO/IEC-7501-1) passport are yet supported.
		printf "$FUNCNAME: Error: "$"Unsupported MRZ"" '%s...'.\n"  "${mrz:0:1}" >&2
		return 1
	fi
	if [[ ${#mrz} != 88 ]] ; then
		printf "$FUNCNAME: %s: "$"Invalid MRZ lenght:"" %d/88.\n" "$errlvl" "${#mrz}" >&2
		[[ "$errlvl" == "Warning" ]] || ret=1
	fi

	doc=(
		[type]=${mrz:0:2}
		[country]=${mrz:2:3}
		[all_names]=${mrz:5:39}
		[number]=${mrz:44:9}
		[check_number]=${mrz:53:1}
		[nationality]=${mrz:54:3}
		[date_of_birth]=${mrz:57:6}
		[check_date_of_birth]=${mrz:63:1}
		[sex]=${mrz:64:1}
		[expiration_date]=${mrz:65:6}
		[check_expiration_date]=${mrz:71:1}
		[personal_number]=${mrz:72:14}
		[check_personal_number]=${mrz:86:1}
		[composite]="${mrz:44:10}${mrz:57:7}${mrz:65:20}"
		[check_composite]=${mrz:87:1}
	)


	for ch in number date_of_birth expiration_date personal_number composite ; do
		doc[checked_$ch]=$(_bl_icao9303_mrz_checkdigit "${doc[$ch]}")
		doc[valid_$ch]=$( [[ ${doc[checked_$ch]} == ${doc[check_$ch]} ]] && echo true || echo false )
		if ! ${doc[valid_$ch]} ; then
			printf "$FUNCNAME: %s: %s checksum -> %s. "$"Should be"" %s.'\n" "$errlvl" "$ch" "${doc[check_$ch]}" "${doc[checked_$ch]}" >&2
			[[ "$errlvl" == "Warning" ]] || ret=1
		fi
	done
	[[ -z "$outdec" ]] || echo "${doc[@]@A}"

	return $ret
}

_bl_pgp_get_seckeyid() {
	local helpmsg="Usage: $FUNCNAME [NAME|U4|U5|EMAIL]
Call 'gpg --with-colon' and ouput exportable secret keys (all if no argument), one by line:
KeyID     Creation_Date    Public_Key_Algorithm

OPTIONS:
  -H, --homedir GNUPGHOME   GnuPG home directory - Environment variable: GNUPGHOME, default: '~/.gnupg'
  -e, --exportable          Ouput only exportable secret keys (ie. present localy, excluding those on security token)
"
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local filtertest=true
	for ((;$#;)) ; do
		case "$1" in
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-e|--exportable) filtertest='[[ "${record[TockenSN]}" == \+ ]]' ;;
			-h|--h*) echo "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$FUNCNAME" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	# See: https://www.rfc-editor.org/rfc/rfc9580#name-public-key-algorithms
	local -a pka=([1]="RSA" "RSA_Encrypt-Only" "RSA_Sign-Only" [16]="Elgamal_Encrypt-Only" "DSA" "ECDH" "ECDSA" "Elgamal" "Diffie-Hellman" "EdDSALegacy" "AEDH" "AEDSA" "X25519" "X448" "Ed25519" "Ed448")
	local pkaindex
	local -A record

	# See: https://git.gnupg.org/cgi-bin/gitweb.cgi?p=gnupg.git;a=blob_plain;f=doc/DETAILS
	gpg --homedir "$gpghome" --list-secret-keys --with-colons $1 | grep "^sec:" | while	IFS=':' read record[Type] record[Validity] record[KeyLenght] record[PubKeyAlgo] record[KeyID] record[CreationDate] record[ExpirationDate] record[Field8] record[Ownertrust] record[UserID] record[SignatureClass] record[KeyCapabilities] record[Field13] record[Flags] record[TockenSN] record[HashAlgo] record[CurveName] record[ComplianceFlags] record[LastUpdate] record[Origin] record[Comment] record[rfu] ; do
		if eval "$filtertest" ; then
			if ! pkaindex=$((${record[PubKeyAlgo]})) ; then
				printf "$FUNCNAME: Error: "$"Unexpected field 4 (Public Key Algorithm)"".\n" >&2
				return 1
			fi
			echo "${record[KeyID]}  $(date --iso-8601=hours --date "@${record[CreationDate]}")  ${pka[${pkaindex}]:-unkown}"
		fi
	done
	return ${PIPESTATUS[0]}
}

_bl_pgp_choose_seckeyid() {
	local qtxt=$"Select KeyID (your OpenPGP certificate)"
	local helpmsg="Usage: $FUNCNAME [NAME|U4|U5|EMAIL]
Output keyid choosen from available (main) secrets keys.
If there is only one available, output its keyid whithout asking.
Argument NAME|U4|U5|EMAIL allow to make an (almost totaly useless) input filter.

OPTIONS:
  -H, --homedir GNUPGHOME   GnuPG home directory (default: ~/.gnupg). Environment variable: \$GNUPGHOME
  -t, --text QUESTION       Text to introduce selection's radiolist (default: '$qtxt')
  -e, --exportable          Only exportable secret keys (ie. present localy, excluding those on security token)
"
	local keyid gpghome=${GNUPGHOME:-~/.gnupg} getargs outfpr=0
	for ((;$#;)) ; do
		case "$1" in
			-F|--fingerprint) outfpr=1 ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-t|--text)
				shift ; qtxt=$1 ;;
			-e|--exportable) getargs+="$1 " ;;
			-h|--h*) echo "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$FUNCNAME" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local -a seckeyids
	mapfile -t seckeyids < <(_bl_pgp_get_seckeyid --homedir "$gpghome" $getargs -- "$1")
	((${#seckeyids[@]}>0)) || { printf "$FUNCNAME: Error: "$"No editable OpenPGP certificate (in %s)"".\n" "$gpghome" >&2 ; return 1 ; }
	if ((${#seckeyids[@]}==1)) ; then
		keyid=${seckeyids[0]}
		printf "$FUNCNAME: Notice: "$"Choosing the only one editable OpenPGP certificate (%s)"".\n" "$keyid" >&2
	else
		keyid="$(bl_radiolist --output-value --num-per-line 1 --text "$qtxt" "${seckeyids[@]}")"
	fi
	echo "${keyid%% *}"
}

_bl_pgpid_eids_of() {
	local helpmsg="Usage: gpg --with-colons … | $FUNCNAME [VALIDITY_ERE]
Print the distinct eids (u4…/u5…, rendered like everywhere else) carried by the
uid records of a 'gpg --with-colons' listing read on standard input, one per line,
sorted.
VALIDITY_ERE, when given, is an ERE the uid validity field (colon column 2) must
fully match for that uid to count. Default: any single validity.
"
	case "$1" in -h|--h*) echo "$helpmsg" ; return ;; esac
	local vf="${1:-.}"
	awk -F: -v vf="$vf" '$1=="uid" && $2 ~ ("^(" vf ")$") { print $10 }' \
		| sed --silent --regexp-extended "s,.*${BL_PGPID_EID_DEPRECATED_REGEX}.*,u\3\5,p ; s,UID.*${BL_PGPID_EID_STANDARD_REGEX}.*,\1,p" \
		| sort -u
}

# RFC 6350 §3.4 escape of a vCard text value carried inside a uid : always the
# backslash and a newline (kept '\n' — a uid is single-line), the comma, and the
# semicolon UNLESS $2 is non-empty (ADR keeps its structural ';'). Output on stdout.
_bl_pgpid_vcard_esc() {   # VALUE [KEEP_SEMICOLON] → escaped
	local v="${1//\\/\\\\}" ; v="${v//$'\r'/}" ; v="${v//,/\\,}"
	[[ "$2" ]] || v="${v//;/\\;}"
	printf '%s' "${v//$'\n'/\\n}"
}

_bl_pgpid_revoke_uid() {
	# $3 = the real (already unescaped) uid string to revoke inside $2 certificate.
	# $5 = 1 to skip the irreversibility confirmation (--yes / non-interactive callers).
	local gpghome=$1 user=$2 ruid=$3 gpgfilter=${4:-false} assumeyes=${5:-0}
	# A revocation is IRREVERSIBLE : OpenPGP keeps the revoked User ID on the
	# certificate forever (marked revoked), and gpg refuses to add an identical
	# one afterwards. Confirm, showing the full uid, unless the caller assumed yes.
	if (( ! assumeyes )) ; then
		local warn=$"Revoking a User ID is IRREVERSIBLE: OpenPGP keeps it on the certificate forever (marked revoked), and an identical User ID can never be added again."
		bl_yesno --default=no --text "$warn"$'\n\n'"  $ruid" $"Revoke this User ID" \
			|| { printf "${FUNCNAME[1]}: Notice: "$"Revocation cancelled.""\n" >&2 ; return 1 ; }
	fi
	printf "${FUNCNAME[1]}: Notice: "$"Revoking %s inside %s certificate…""\n" "$ruid" "$user" >&2
	gpg --homedir "$gpghome" --batch --quick-revoke-uid "$user" "$ruid" 2> >( $gpgfilter || grep "^\(\[\|gpg:\)" >&2 ) || { echo "${FUNCNAME[1]}: Error: "$"Can't revoke - good PIN ?." >&2 ; return 1 ;}
}

_bl_pgpid_fix_primary() {
	local helpmsg="Usage: $FUNCNAME GNUPGHOME KEY [STDERRFILTER]
Ensure KEY still announces an email address as its main identity, ie. as the first
uid gpg lists - what OpenPGP tools and MTAs read. Move the primary-user-id flag
onto the newest remaining address uid when it does not, do nothing when it does.
Meant to run after revoking a uid: gpg leaves the flag on the revoked one and
silently falls back on the newest remaining uid, whatever its shape.
See bl-pgpid-implementation(7)
"
	case "$1" in -h|--h*) echo "$helpmsg" ; return ;; esac
	local gpghome=$1 user=$2 gpgfilter=${3:-false}
	local created rawu u newest= newestdate=0 first=1
	while IFS=: read -r created rawu ; do
		u=$(printf '%b' "$rawu")
		if ((first)) ; then
			first=0
			[[ ! "$u" =~ \<[^\<\>]+\>[[:space:]]*$ ]] || return 0
		fi
		[[ "$u" =~ \<[^\<\>]+\>[[:space:]]*$ ]] || continue
		# Newest self-sig creation date wins, last listed wins ties.
		(( created < newestdate )) || { newestdate=$created ; newest=$u ; }
	done < <(gpg --homedir "$gpghome" --with-colons --list-secret-key "$user" | awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" && $2 ~ /^[ounmfqws-]/ { printf "%s:%s\n", $6, $10 }')
	[[ "$newest" ]] || { printf "${FUNCNAME[1]}: Warning: "$"No address left on %s certificate to carry the primary User ID flag.""\n" "$user" >&2 ; return 0 ; }
	printf "${FUNCNAME[1]}: Notice: "$"Main identity is no longer an address: moving the primary User ID flag to %s.""\n" "$newest" >&2
	gpg --homedir "$gpghome" --batch --quick-set-primary-uid "$user" "$newest" 2> >( $gpgfilter || grep "^\(\[\|gpg:\)" >&2 ) || { echo "${FUNCNAME[1]}: Error: "$"Can't set the primary User ID - good PIN ?." >&2 ; return 1 ;}
}

_bl_pgpid_vfold() {
	# Emit a vCard content line folded to ≤75 octets (RFC 6350 §3.2) : CRLF then a
	# single leading space starts each continuation.
	local l=$1
	printf '%s\r\n' "${l:0:75}" ; l=${l:75}
	while [[ "$l" ]] ; do printf ' %s\r\n' "${l:0:74}" ; l=${l:74} ; done
}

_bl_pgpid_ks_to_keyurl() {
	# KEYSERVER_URI FPR → the op=get lookup URL for a vCard KEY;MEDIATYPE (nothing
	# when unparseable). hkps→https (443 implicit) ; hkp→http, its default port
	# 11371 made explicit (http would else hit 80) unless the URI already has one.
	local ks=$1 fpr=$2 scheme hostport
	[[ "$ks" =~ ^(hkps?|https?)://([^/]+) ]] || return 0
	scheme=${BASH_REMATCH[1],,} ; hostport=${BASH_REMATCH[2]}
	[[ "$scheme" != hkp || "$hostport" == *:* ]] || hostport="$hostport:11371"
	case "$scheme" in
		hkps|https) printf 'https://%s/pks/lookup?op=get&search=0x%s\n' "$hostport" "$fpr" ;;
		*)          printf 'http://%s/pks/lookup?op=get&search=0x%s\n' "$hostport" "$fpr" ;;
	esac
}

_bl_pgpid_upgrade_uids() {
	local helpmsg="Usage: $FUNCNAME GNUPGHOME KEY [STDERRFILTER]
Upgrade a legacy certificate: mint its identity uid UID:urn:eid:<eid>, and an FN:
carrying the name part of the first legacy uid. Legacy uids are neither revoked
nor re-minted, the web of trust rests on their certifications, and the primary
flag stays where its holder put it.
No-op without the secret key, without a single consistent legacy eid, or when the
identity uid already exists.
See bl-pgpid-implementation(7)
"
	case "$1" in -h|--h*) echo "$helpmsg" ; return ;; esac
	local gpghome=$1 user=$2 gpgfilter=${3:-false}
	local rawu u fn eiduid
	local -a uids=() ids
	while IFS= read -r rawu ; do
		u=$(printf '%b' "$rawu")
		[[ ! "$u" =~ ^UID:urn:eid: ]] || return 0	# already vCard-uid shaped
		uids+=("$u")
	done < <(gpg --homedir "$gpghome" --with-colons --list-secret-key "$user" 2>/dev/null \
		| awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" { i+=1 } $1=="uid" && $2 ~ /^[ounmfqws-]/ { print $10 }')
	(( ${#uids[@]} )) || return 0	# no secret key or no usable uid : not ours to upgrade
	ids=($(printf '%s\n' "${uids[@]}" | sed --silent --regexp-extended "s,.*${BL_PGPID_EID_DEPRECATED_REGEX}.*,u\3\5,p" | sort -u))
	(( ${#ids[@]} == 1 )) || return 0	# no (or conflicting) legacy eid : nothing to upgrade

	printf "${FUNCNAME[1]}: Notice: "$"Upgrading %s certificate to the vCard-uid process (legacy uids kept : the web of trust rests on them)…""\n" "$user" >&2
	local -a GQ=(gpg --homedir "$gpghome" --batch)
	eiduid="UID:urn:eid:${ids[0]}"
	printf "%s: Notice: Adding %s into %s certificate...\n" "${FUNCNAME[1]}" "'$eiduid'" "$user" >&2
	"${GQ[@]}" --quick-add-uid "$user" "$eiduid" 2> >( $gpgfilter || grep "^\( *[A-Z«]\|gpg:\)" >&2 ) || { printf "%s: Error: Can't %s - good PIN ?.\n" "${FUNCNAME[1]}" "adduid" >&2 ; return 1 ;}

	# Name part of the first legacy uid (its comment and email stripped).
	fn=${uids[0]%%\ \(*} ; fn=${fn%%\ \<*} ; fn=${fn# } ; fn=${fn% }
	if [[ "$fn" && "$fn" != *\<* ]] ; then
		printf "%s: Notice: Adding %s into %s certificate...\n" "${FUNCNAME[1]}" "'FN:${fn}'" "$user" >&2
		"${GQ[@]}" --quick-add-uid "$user" "FN:${fn}" 2> >( $gpgfilter || grep "^\( *[A-Z«]\|gpg:\)" >&2 ) || { printf "%s: Error: Can't %s - good PIN ?.\n" "${FUNCNAME[1]}" "adduid" >&2 ; return 1 ;}
	fi

	# Emails left alone : a legacy uid is already name-addr, and a second uid for
	# the same address would split its third-party certifications.

	# The primary flag is NOT touched — bl-pgpid-implementation(7).
}


### public functions / program actions ###

bl_pgpid_get() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... NAME|U4|U5|EMAIL..."
	local maxfromks=100
	local keyserv keyservs=("${BL_PGPID_KEYSERVERS[@]}")
	local helpmsg="
"$"Output fingerprints, emails and eid of certificates matching NAME|U4|U5|EMAIL.""
"$"May also get or refresh certificates from keyservers.""

OPTIONS:
  -F, --fingerprint           "$"Output only fingerprints""
  -E, --email                 "$"Output only emails""
  -f, --no-fetch              "$"Don't refresh certificates from keyservers or Web Key Directories""
  -s, --secret                "$"Search through local secret keys instead of public certificate."" - "$"Imply --no-fetch""
  -m, --errexit-g=1           "$"Return an error if there is more than one (1) entry."" - "$"You may replace '1' by an other number""
  -M, --max-from-keyserv MAX  "$"Set maximum matching certificates to receive from keyservers. "$"Default: ""$maxfromks
  -r, --recurse[=NUM]         "$"Also fetch & display the certs signing the target(s), NUM levels deep (default 1, max 6)""
      --import-clean          "$"After import, remove any signatures from the certificate that are not usable, including from certificates that are not present on the keyring""
      --import-minimal        "$"Import the smallest certificate possible, removing almost all signatures""
  -H, --homedir GNUPGHOME     "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME
  -K, --keyservers KEYSERVERS "$"Search and refresh certificates from this keyservers. "$"Default: ""
$(printf -- "%80s\n" ${keyservs[@]})
"
	local onlyfpr=0 onlymbox=0
	local gpgaction="--list-keys"
	local wkdcmd="gpg --auto-key-locate clear,wkd --locate-external-keys"
	local errexit=$(getconf INT_MAX)
	local importclean="no-import-clean" importminimal="no-import-minimal"
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local recurse=0 _recursing=0
	local -a passopts=()                       # options propagated to --recurse self-calls
	for ((;$#;)) ; do
		case "$1" in
			-F|--fpr|--fingerprint)
				onlyfpr=1 onlymbox=0 ; passopts+=(--fingerprint) ;;
			-E|--mbox|--email)
				onlyfpr=0 onlymbox=1 ; passopts+=(--email) ;;
			-s|--secret)
				gpgaction="--list-secret-keys" ;&
			-l|-f|--local|--no-fetch)
				wkdcmd="true"
				passopts+=(--no-fetch)
				keyservs=() ;;
			-m|--errexit|--errexit-g)
				errexit=1 ;;
			--errexit*=*)
				errexit=${1#*=}
				[[ "$errexit" =~ ^[1-9][0-9]*$ ]] || { printf "$FUNCNAME: Error: "$"Option %s expect a number"".\n" "--errexit-g=" >&2 ; return 2 ; }
				;;
			-M|--max-from-keyserv)
				[[ "$2" =~ ^[0-9]+$ ]] || { printf "$FUNCNAME: Error: "$"Option %s expect a number"".\n" "--max-from-keyserv" >&2 ; return 2 ; }
				(($2 <= 100)) || printf "$FUNCNAME: Warning: "$"%s exceeds usual key server limit %s"".\n" "$2" "(100)" >&2
				shift ; maxfromks=$1 ;;
			-r|--recurse) recurse=1 ;;
			--recurse=*)
				recurse=${1#*=}
				[[ "$recurse" =~ ^[0-6]$ ]] || { printf "$FUNCNAME: Error: "$"Option %s expects a number in 0..6"".\n" "--recurse=" >&2 ; return 2 ; } ;;
			--_recursing) _recursing=1 ;;   # internal: this call is a --recurse descendant
			--import-clean)
				importclean="import-clean" ; passopts+=(--import-clean) ;;
			--import-minimal)
				importminimal="import-minimal" ; passopts+=(--import-minimal) ;;
			-K|--keyservers)
				shift ; keyservs=($1) ; passopts+=(--keyservers "$1") ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ; passopts+=(--homedir "$gpghome") ;;
			-v|--verify) verify=1 ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	[[ "$1" ]] || { printf "$usage\n" >&2 ; return 2 ; }
	local users=("$@")

	if ((maxfromks>0)) ; then
		local -a ksfprs user
		local scheme serv port path ksurl
		for keyserv in ${keyservs[@]} ; do
			[[ "$keyserv" =~ // ]] || keyserv="hkps://$keyserv"
			[[ "${keyserv,,}" =~ ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*) ]] || { printf "$FUNCNAME: Warning: "$"Non RFC3986 URI, skipping  %s.""\n" "'$keyserv'" ; continue ; }
			scheme=${BASH_REMATCH[2]}
			serv=${BASH_REMATCH[4]}
			path=${BASH_REMATCH[5]}
			[[ $scheme == hkp ]] || [[ $scheme == hkps ]] ||{ printf "$FUNCNAME: Warning: "$"Non hkp or hkps scheme, skipping  %s.""\n" "'$keyserv'" ; continue ; }
			if [[ "${serv##*:}" =~ ^[0-9]+$ ]] ; then
				port=${BASH_REMATCH[0]}
				serv=${serv%:*}
			else
				[[ "${scheme:3:1}" == s ]] && port=443 || port=11371
			fi
			ksurl="http${scheme:3:1}://${serv}:${port}${path}/pks/lookup"
			for user in "${users[@]}" ; do
				# If input seems to be a KeyID (len=16) or v4 fpr (len=40) or v6 fpr (len=64) without is "0x" header, add it as keyserver use to need it.
				! [[ "$user" =~ ^([0-9abcdefABCDEF]{24}){0,2}[0-9abcdefABCDEF]{16}$ ]] || user="0x$user"
				ksfprs=($(curl --no-progress-meter --location --get --data-urlencode 'op=index' --data-urlencode "search=$user" --data-urlencode "options=mr" "$ksurl" | sed -n 's,^pub:\([[:xdigit:]]*\):.*,\1,p'))
				if [[ -z "${ksfprs[@]}" ]] ; then
					printf "$FUNCNAME: Notice: "$"No certificate for %s on %s.""\n" "'$user'" "'$keyserv'" >&2
					continue
				elif ((${#ksfprs[@]} <= maxfromks )) ; then
					curl --no-progress-meter --location --get --data-urlencode 'op=get' --data-urlencode "search=$user" --data-urlencode "options=mr" "$ksurl" | gpg --homedir "$gpghome" --import-options ${importclean},${importminimal} --import || true
				else
					local i
					for ((i=0;i<maxfromks;i++)) ; do
						curl --no-progress-meter --location --get --data-urlencode 'op=get' --data-urlencode "search=0x${ksfprs[i]}" --data-urlencode "options=mr" "$ksurl" | gpg --homedir "$gpghome" --import-options ${importclean},${importminimal} --import
					done
				fi
			done
		done
	fi
	$wkdcmd --homedir "$gpghome" --import-options ${importclean},${importminimal} $(grep -o --extended-regexp "$BL_INTERACTIVE_EMAIL_REGEX" <<<"${users[@]}") || true

	local -a fprmboxs fprs
	readarray -t fprmboxs < <(gpg --homedir "$gpghome" --list-options show-only-fpr-mbox $gpgaction -- "${users[@]}" | sort -u )
	fprs=($(printf -- "%s\n" "${fprmboxs[@]%% *}" | sort -u))
	(( ${#fprs[@]} > 0 )) || { printf "$FUNCNAME: Error: "$"No certificate for"" '%s'...\n" "$users" >&2 ; return 141 ;}
	if ((onlyfpr)) ; then
		printf -- "%s\n" "${fprs[@]}"
	elif ((onlymbox)) ; then
		printf -- "%s\n" "${fprmboxs[@]#* }"
	else
		local cl cfpr prevfpr id
		for cl in "${fprmboxs[@]}" ; do
			cfpr=${cl%% *}
			if [[ "$cfpr" != $prevfpr ]] ; then
				id=" $(gpg --homedir "$gpghome" --with-colons $gpgaction "0x$cfpr" | _bl_pgpid_eids_of)"
				if [[ "$id" == *$'\n'* ]] ; then
					printf "$FUNCNAME: Warning: "$"Certificate %s contains more than one eid.""\n" "0x$cfpr" >&2
					id=" -"
				elif [[ "$id" == \  ]] ; then
					id=" -"
				fi
			fi
			printf -- "%-80s${id}\n" "$cl"
			prevfpr=$cfpr
		done
	fi

	# --recurse[=NUM]: also fetch & display the certs signing the target(s), up to NUM levels deep.
	# Self-recursion (DRY) ; BL_PGPID_GET_SEEN dedups signers over the whole walk (perf-safe on cyclic WoT).
	if ((recurse > 0)) ; then
		declare -gA BL_PGPID_GET_SEEN
		((_recursing)) || BL_PGPID_GET_SEEN=()
		local kid gf
		for gf in "${fprs[@]}" ; do BL_PGPID_GET_SEEN[${gf: -16}]=1 ; done
		for kid in $(gpg --homedir "$gpghome" --with-colons --list-sigs $(printf -- '0x%s ' "${fprs[@]}") 2>/dev/null | awk -F: '$1=="sig"{print $5}' | sort -u) ; do
			[[ ! -v BL_PGPID_GET_SEEN[$kid] ]] || continue
			BL_PGPID_GET_SEEN[$kid]=1
			"$FUNCNAME" "${passopts[@]}" --_recursing --recurse=$((recurse-1)) "0x$kid" || true
		done
	fi

	if ((${#fprs[@]} != 1 )) ; then
		(( ${#fprs[@]} <= errexit )) && local errlvl="Notice" || local errlvl="Error"
		printf "$FUNCNAME: $errlvl: "$"%s certificates for %s input params." "${#fprs[@]}" "${#users[@]}" >&2
		(( ${#fprs[@]} >= maxfromks )) && printf " "$"Some public certificates may be missing.""\n" >&2 || printf "\n" >&2
	fi
	(( ${#fprs[@]} <= errexit )) || return 7
}

bl_pgpid_mrz_to_u4() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [MRZ]..."
	local helpmsg="
Calculate and output a OpenPGP ID u4, from the Machine Readable Zone of an icao9303 passport.
If MRZ is not passed as argument, it will we read from STDIN.

OPTIONS:
  -d, --birth-date YYYY-MM-DD  "$"Birth date, expected format : Year-Month-Day (for people born 100+ years ago)""
  -u, --uncheck                "$"Change some MRZ errors to warnings (only if you really know what you do !)""

WARNING: mrz data may be irrelevant to generate u4 string. eg:
         * Surname or given names may be incomplete (cut bc exceed mrz size)
         * Surname or given names may differs from those given at birth (marriage, gender change, etc.)
         * Surname or given names transliteration may have change over time
         * Year of birth date is only written with 2 digits (and people may live longer than 100 years)
         * Humans may have done error on birth date, surname or given names.
By the way, there are ~80% chance that an icao9303 MRZ allow to generate a correct u4 string.
People have to check, and fix when NOK.
"

	local gdate uncheck
	for ((;$#;)) ; do
		case "$1" in
			-d|--birth-d*) shift ; gdate=$(sed ' s,[^0-9],,g ' <<<"$1") ;; # keep only [0-9] from given input
			-u|--uncheck) uncheck="--uncheck" ;;
			-h|--h*) echo "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) echo -e "$name: Error:" $"Unrecognized option/parameters"" $1\n$helpmsg" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local -A mrza
	if ! _bl_icao9303_mrz_analyse $uncheck mrza "$@" ; then
		echo "$FUNCNAME: Error:" $"Invalid Machine Readable Zone (ICAO9303)" >&2
		return 1
	fi

	local allnames bdate countrycode coordinates

	if ! allnames=$(LC_COLLATE="C.UTF-8" grep -o "[A-Z]\{1,32\}<<[A-Z]\{1,32\}<[A-Z]\{0,32\}<" <<<"${mrza[all_names]}" ) ; then
		echo "$FUNCNAME: Error:" $"No valid and complete surname and given names extracted from" "${mrza[all_names]}" >&2
		return 1
	fi

	countrycode="${mrza[country]}"
	coordinates="${BL_PGPID_COORDINATES[$countrycode]: 0:14}"
	if ! [[ "$coordinates" =~ ^${BL_PGPID_CO_REGEX}$ ]] ; then
		printf "$FUNCNAME: Error: "$"Invalid coordinates (%s) for country '%s'.""\n" "$coordinates" "$countrycode" >&2
		return 1
	fi

	if [[ "$gdate" ]] ; then
		((${#gdate}==8)) || printf "$FUNCNAME: Notice: "$"Invalid lenght for birth date '%s'.""\n" "$gdate" >&2
	else
		gdate="${mrza[date_of_birth]}"
	fi
	if ! bdate=$(date --date "${gdate}" +"%Y-%m-%d" ) ; then
		printf "$FUNCNAME: Error: "$"Can't reformat birthdate '%s'.""\n" "$gdate" >&2
		return 1
	fi

	echo "$(printf "${allnames}${bdate}" | md5sum | xxd -r -p | basenc --base64url | sed 's/==$//')$coordinates"
}

bl_pgpid_gen_u4() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [MRZ]..."
	local helpmsg="
"$"Generate an eid u4 string"". "$"Missing input will be asked interactively.""
"$"You may pass the Machine Readable Zone of an international passport as argument, but there are ~20% chances that generated *u4* is incorrect.""

OPTIONS:
  -s, --surname SURNAME            "$"Surname/family name at birth"".
  -g, --given-names GIVEN_NAMES    "$"Given names at birth, separated by space ' ' or comma ',' or hyphen '-'""
  -d, --birth-date YYYY-MM-DD      "$"Birth date, expected format : Year-Month-Day"".
  -c, --birth-country COUNTRY_CODE "$"3 letters country code of birth place: GBR, NGA, FRA, …""
  -v, --verify                     "$"Ask for verification"".
"
	local sname gname translit bdate bcountry errlvl="Error"
	for ((;$#;)) ; do
		case "$1" in
			-s|--s*)
				shift ; sname=$1 ;;
			-g|--g*)
				shift ; gname=$1 ;;
			-d|--birth-d*)
				shift ; bdate=$1 ;; # $bdate is always be passed as an argument to bl_pgpid_mrz_to_u4()
			-c|--birth-c*)
				shift ; bcountry=$1 ;;
			-v|--verify) errlvl="Warning" ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	# If a Machine Readable Zone is passed as arguments
	if [[ "$1" ]] ; then
		local -A mrza=()
		if ! _bl_icao9303_mrz_analyse mrza "$@" ; then
			printf "$FUNCNAME: %s: "$"Invalid Machine Readable Zone"" (ICAO9303)\n" "$errlvl" >&2
			# If not --verify, errexit.
			[[ "$errlvl" == Warning ]] || return 1
		fi
		[[ "$sname" ]] || sname=$(tr '<' ' ' <<<"${mrza[all_names]%%<<*}")
		[[ "$gname" ]] || gname=$(sed ' s,<*$,,g ; s,<, , ' <<<"${mrza[all_names]#*<<}")
		[[ "$bdate" ]] || bdate=$(date --iso-8601 --date "${mrza[date_of_birth]}")
		[[ "$bcountry" ]] || bcountry="${mrza[country]}"
	fi

	[[ "$sname" ]] || sname=$(bl_input $"Birth surname (family name)")
	[[ "$gname" ]] || gname=$(bl_input $"Birth names (all given names)")
	[[ "$bdate" ]] || bdate=$(bl_input --iso-8601 $"Birth date (YYYY-MM-DD)")
	[[ "$bcountry" ]] || bcountry=$(bl_input --default "FRA" $"Birth country (3 letter code)")

	# --verify
	while [[ "$errlvl" == Warning ]] ; do
		! bl_yesno --default=no --text \
	"
	"$"Surname at birth:""     ${sname}
	"$"Given names at birth:"" ${gname}
	"$"Date of birth:""        ${bdate}
	"$"Country of birth:""     ${bcountry^^}
	" \
			$"Is that correct" || break

		sname=$(bl_input --default "${sname^^}" $"Birth surname (family name)")
		gname=$(bl_input --default "${gname}" $"Birth names (all given names)")
		bdate=$(bl_input --iso-8601 --default "${bdate}" $"Birth date (YYYY-MM-DD)")
		bcountry=$(bl_input --default "${bcountry^^}" $"Birth country (3 letter code)")
	done

	sname=$(sed 's,[ ;,<-]\+,<,g'<<<"$sname" ) # replace and squeeze some word separators
	translit=$(iconv -f utf-8 -t ascii//TRANSLIT <<<"$sname") # transliterate (according to iconv), may differ from Passport transliterations.
	if [[ "$translit" != "$sname" ]] ; then
		printf "$FUNCNAME: Warning: "$"'%s' has been transliterated to '%s'. "$"It may be WRONG !""\n" "$sname" "$translit" >&2
		sname="$translit"
	fi
	sname="${sname^^}" # uppercase

	gname=$(sed 's,[ ;,<-]\+,<,g'<<<"$gname" )
	translit=$(iconv -f utf-8 -t ascii//TRANSLIT <<<"$gname")
	if [[ "$translit" != "$gname" ]] ; then
		printf "$FUNCNAME: Warning: "$"'%s' has been transliterated to '%s'. "$"It may be WRONG !""\n" "$gname" "$translit" >&2
		gname="$translit"
	fi
	gname="${gname^^}"

	if ! fakemrz="P<${bcountry^^}$(LC_COLLATE="C.UTF-8" grep -o "[A-Z]\{1,32\}<<[A-Z]\{1,32\}<[A-Z]\{0,32\}<" <<<"${sname}<<$gname<<" )" ; then
		echo "$FUNCNAME: Error: "$"Only [A-Z] characters allowed in names." >&2
		return 1
	fi
	fakemrz+="000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"

	bl_pgpid_mrz_to_u4 --uncheck --birth-date "$bdate" "${fakemrz: 0:88}"
}

bl_pgpid_gen_uid() {
	# Range [BL_PGPID_XUID_MIN, BL_PGPID_XUID_MAX] : uids at or above 2^31 break
	# on software that treats them as signed — bl-pgpid-implementation(7).
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [U4]"
	local helpmsg="
"$"Generate a 32bit Unix User ID, from 2^18 to (2^31)-2"" ([$BL_PGPID_XUID_MIN,$BL_PGPID_XUID_MAX]).
"$"If no argument is given, it will ask for surname, names and date of birth.""

OPTIONS:
  -f, --free-input            "$"Accept any input, not only valid PGPID U4 string""
  -F, --from FILE             "$"Get input from first line of FILE (eg: fifo, tmpfs, /dev/stdin …)""
"
	local freeinput=0 str_in u4h num
	for ((;$#;)) ; do
		case "$1" in
			-f|--free-input) freeinput=1 ;;
			-F|--from)
				shift ; str_in=$( sed 1q < "$1" ) || return 1 ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	if [[ -z "$str_in" ]] ; then
		if [[ "$1" ]] ; then
			# input is passed as arguments
			str_in="$*"
		else
			# No input at all (then --free-input is non-sense)
			str_in="$(bl_pgpid_gen_u4 --birth-country FRA)==" || return $?
			freeinput=0
		fi
	elif [[ "$1" ]] ; then
			printf "$FUNCNAME: Warning: "$"Option %s overide given argument '%s'""...\n" "--from" "${1: 0:44}" >&2
	fi

	if ((freeinput)) ; then
		u4h="$(echo "$str_in" | md5sum | xxd -r -p | basenc --base64url)"
	elif [[ "$str_in" =~ (${BL_PGPID_U4H_REGEX})${BL_PGPID_CO_REGEX} ]] ; then
		u4h="${BASH_REMATCH[1]}=="
	elif [[ "$str_in" =~ ${BL_PGPID_U5_REGEX} ]] ; then
		u4h="$(echo "${BASH_REMATCH[0]}" | md5sum | xxd -r -p | basenc --base64url)"
	else
		printf "$FUNCNAME: Error: "$"No eid (u4 or u5) found in '%s'""...\n" "${str_in: 0:50}" >&2
		return 2
	fi

	num=$(bl_shrink_num --input-encoding base64url $((BL_PGPID_XUID_MAX - BL_PGPID_XUID_MIN)) <<<"$u4h") || return $?
	echo $((num + BL_PGPID_XUID_MIN))
}

bl_pgpid_gen_key() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... EMAIL"
	local keyserv="${BL_PGPID_KEYSERVERS[0]}"
	local helpmsg="
"$"Generate an OpenPGP key pair (public and secret) according to OpenPGP ID standards.""
"$"Missing input will be asked interactively.""
"$"Output 3 lines for each fingerprints:""
* "$"main key"" (Sign Certify)
* "$"decryption key"" (Encrypt)
* "$"authentication key"" (Auth)

OPTIONS:
  -N, --name PSEUDONYM             "$"Common name or pseudonym. "$"Default: "$"first part of email"".
  -c, --eid U4|U5                  "$"Entity ID. Worldwide and decentralised entity identifier."$" See: ""\$BL_PGPID_U4_REGEX \$BL_PGPID_U5_REGEX.
  -C, --extra-comment NOTE         "$"Supplemental information or comment associated with the entity.""
  -p, --passphrase PASSPHRASE      "$"Passphrase to (symetric) encrypt secret part of OpenPGP key. CAN'T BE EMPTY (at this stage)""
  -P, --passfrom FILE              "$"Get passphrase from first line of FILE (eg: fifo, tmpfs, /dev/stdin …)""
  -e, --expiration YEARS           "$"Number of years before certificate expiration. "$"Default: ""11 -> $(date -I -d "+11 years"))
  -k, --keyserver KEYSERVER        "$"Prefered OpenPGP certificate server. "$"Default: ""$keyserv
  -H, --homedir GNUPGHOME          "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME
"
	# TODO: (maybe) sanitize --name or --extra-comment inputs (removing '\' '(' ')' ... ) ?
	local email passphrase pseudo pgpidu pgpidugiven extracomment
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local expire=11 verify=""
	for ((;$#;)) ; do
		case "$1" in
			-n|-N|--name*)
				shift ; pseudo=${1:-} ;;
			-c|--comment|--eid)
				shift ; pgpidu=$1 ; pgpidugiven="." ;;
			-C|--extracomment|--extra-comment)
				shift ; extracomment=$1 ;;
			-p|--passphrase)
				shift ; passphrase=${1:?} ;;
			-P|--passfrom|--pass-from)
				shift ; passphrase=$( sed 1q < "$1" ) || return 1 ;;
			-e|--expir*)
				[[ "$2" =~ ^[1-9][0-9]*$ ]] || { printf "$FUNCNAME: Error: "$"Given parameter '%s' is not a valid number.""\n" "$2" >&2 ; return 2 ;}
				shift ; expire=$1 ;;
			-k|--keyserver)
				[[ "$2" =~ ^hkps?:// ]] || { printf "$FUNCNAME: Error: "$"keyserver MUST be hkp(s)://…""\n" >&2 ; return 2 ;}
				shift ; keyserv=$1 ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-v|--verify) verify=1 ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	if ! [[ "$1" =~ ^${BL_INTERACTIVE_EMAIL_REGEX}$ ]] ; then
		printf "$usage\n" >&2
		return 2
	fi
	email="$1"
	shift

#TODO: implement --verify to check input and ask when incoherent or missing.
#TODO (also): check gpg version, because we use some recent feature, and compatibility may change (tested with gpg (GnuPG) 2.2.27 libgcrypt 1.8.8)

	if [[ "$pgpidu" =~ ${BL_PGPID_U4_REGEX} ]] ; then
		pgpidu="u4${BASH_REMATCH[0]}"
	elif [[ "$pgpidu" =~ ${BL_PGPID_U5_REGEX} ]] ; then
		pgpidu="u5${BASH_REMATCH[0]}"
	elif [[ "$pgpidugiven" ]] ; then
		printf "%s: Error: No eid in given '%s'.\n" "$FUNCNAME" "$pgpidu" >&2
		return 2
	else
		pgpidu="u4$(bl_pgpid_gen_u4 --verify)"
	fi

	[[ "$pseudo" ]] || pseudo=${email%%@*}

	[[ "$passphrase" ]] || passphrase=$(bl_new_password "Passphrase")
	[[ "$passphrase" ]] || { printf "%s: Error: Passphrase can't be empty at this stage.\n" "$FUNCNAME" >&2 ; return 2 ;}

	# Born with a single self-certified identity uid (the eid URN, §4.5 of
	# draft-foopgp-urn-eid). Name and note follow as vCard-property uids ; the
	# address is a plain name-addr uid (bl-pgpid-implementation(7)).
	cat <<-EOF | gpg --homedir "$gpghome" --batch --generate-key --allow-freeform-uid || return $?
		%echo Generating OpenPGP key for ${email}
		Key-Type: eddsa
		Key-Curve: Ed25519
		Key-Usage: cert sign
		Subkey-Type: ecdh
		Subkey-Curve: Curve25519
		Subkey-Usage: encrypt
		Name-Real: UID:urn:eid:${pgpidu}
		Expire-Date: $(date -I -d "+$expire years")
		Passphrase: ${passphrase}
		Keyserver: ${keyserv}
		# Do a commit here, so that we can later print 'done' :-)
		%commit
	EOF

	# Look the fresh key up by its exact eid UID ('=' prefix = exact match), the
	# only UID it has at this point.
	local fpr
	fpr=$(gpg --homedir "$gpghome" --list-secret-keys --with-colons "=UID:urn:eid:${pgpidu}" | awk -F: '$1=="fpr"{print $10;exit}') || return $?
	[[ "$fpr" ]] || { printf "$FUNCNAME: Error: "$"Freshly generated key not found by its eid UID"" (%s).\n" "$pgpidu" >&2 ; return 1 ; }

	# Add the vCard-property UIDs, then the email as a plain 'Name <addr>' uid
	# (see _bl_pgpid_upgrade_uids : an 'EMAIL: <addr>' uid breaks real MUAs).
	local -a GQ=(gpg --homedir "$gpghome" --batch --passphrase "${passphrase}" --pinentry-mode loopback)
	"${GQ[@]}" --quick-add-uid "$fpr" "FN:${pseudo}" || return $?
	"${GQ[@]}" --quick-add-uid "$fpr" "${pseudo} <${email}>" || return $?
	[[ -z "${extracomment// }" ]] || "${GQ[@]}" --quick-add-uid "$fpr" "NOTE:$(_bl_pgpid_vcard_esc "${extracomment# }")" || return $?
	# Primary goes on the ADDRESS uid, never on the eid anchor, and the reason it
	# is not the other way round is worth reading: bl-pgpid-implementation(7).
	"${GQ[@]}" --quick-set-primary-uid "$fpr" "${pseudo} <${email}>" || printf "$FUNCNAME: Warning: "$"Can't set the primary uid to"" %s.\n" "'${pseudo} <${email}>'" >&2

	# Generate and add auth key...
	"${GQ[@]}" --quick-add-key "$fpr" ed25519 auth ${expire}y || return $?

	# output all (3) fingerprints, first fingerprint should be the one of the main key
	gpg --homedir "$gpghome" --list-secret-keys --with-colons "$email" | sed -n 's,^fpr:.*:\([[:xdigit:]]\{40\}\):,\1,p'
}

bl_pgpid_avatar() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local tmpdir="/tmp/$FUNCNAME.$USER"
	local usage=$"Usage:"" $name [OPTIONS]... [NAME|EMAIL|KEYID|U4|U5]"
	local keyserv keyservs=("${BL_PGPID_KEYSERVERS[@]}")
	local helpmsg="
"$"Extract or add image inside OpenPGP certificate.""
"$"Missing NAME|EMAIL|KEYID|U4|U5 => Guess it using security token, or interactively ask KEYID of secret key.""
"$"New IMAGE should be 180x180 pixels, or it will be resized.""
"$"Output path(s) of extracted image(s).""

OPTIONS:
  -E, --extract-all           "$"Extract also all revoked or expired existing images from OpenPGP certificate""
  -A, --replace-to IMAGE      "$"Resize and add new IMAGE inside OpenPGP certificate (revoking any previous image)""
  -R, --revoke                "$"Just revoke all existing images inside OpenPGP certificate""
  -W, --workdir DIRECTORY     "$"Working directory. Will contain previous and new resized images. "$"Default: ""$tmpdir .
  -H, --homedir GNUPGHOME     "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME .
  -K, --keyservers KEYSERVERS "$"If non-empty, send updated certificate to this keyservers. "$"Default: ""
$(printf -- "%80s\n" ${keyservs[@]})
"
	local image revoke
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local listopts="show-photos"
	for ((;$#;)) ; do
		case "$1" in
			-E|--extract-all)
				listopts+=",show-unusable-uids" ;;
			-A|--add|--addfrom|--add-from|--replaceto|--replace-to)
				shift ; image=${1:?} ;&
			-R|--revoke)
				revoke="." ;;
			-W|--tmpdir|--workdir)
				shift ; tmpdir=${1:?} ;;
			-k|--keyserver)
				printf -- "$FUNCNAME: Warning: "$"Deprecated option"" '--keyserver' - "$"Please use %s instead.""\n" "'--keyservers'" >&2
				;&
			-K|--keyservers)
				shift ; keyservs=($1) ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local user fprs

	[[ "$1" ]] && user=$1 || user=$(_bl_pgpid_default_key "$gpghome") || return $?

	mkdir -p "$tmpdir" || return $?

	# Backup existing photos and get fingerprints
	echo -n > "$tmpdir/imglist" || return $?
	fprs=($(gpg --homedir "$gpghome" --list-key --list-options "$listopts" --photo-viewer "img=\$(md5sum %i | head -c 32).%t && cp %i ${tmpdir@Q}/\$img && echo ${tmpdir@Q}/\$img >> ${tmpdir@Q}/imglist" "$user" | sed -nE 's,^ *([0-9ABCDEF]{40})$,\1,p' | sort -u ))
	(( ${#fprs[@]} > 0 )) || { printf "$FUNCNAME: Error: "$"No certificate for user '%s'.""\n" "'$user'" >&2 ; return 1 ;}

	if [[ "$image" ]] ; then
		local -a size
		# copy image to read it once if it was a fifo or /dev/stdin ...
		cat "$image" > "$tmpdir/origin.image" || { printf "%s: Error: Can't retrieve image file %s.\n" "$FUNCNAME" "${image@Q}" >&2 ; return 1 ;}
		size=($(gm identify -format "%w %h" "$tmpdir/origin.image")) || { printf "$FUNCNAME: Error: "$"Unable to identify image"" ${image@Q}.\n" >&2 ; return 1 ;}
		if ((size[0]==180 && size[1]==180)) ; then
			# Image has already the expected size, don't alter it.
			mv "$tmpdir/origin.image" "$tmpdir/new.jpg" || return $?
		else
			# Resize
			gm convert -geometry "180^" -gravity center -extent 180 -strip "$tmpdir/origin.image" jpeg:"$tmpdir/new.jpg" || { printf "$FUNCNAME: Error: "$"Unable to resize image"" ${image@Q}.\n" >&2 ; return 1 ;}
			rm "$tmpdir/origin.image" || return $?
		fi
	fi

	local ret=0
	if [[ "$revoke" ]] ; then
		(( ${#fprs[@]} < 2 )) || { printf "$FUNCNAME: Error: "$"User '%s' match many (%d) certificates"" (%s).\n" "$user" ${#fprs[@]} "${fprs[*]}" >&2 ; return 1 ;}

		local commands
		# Revoke non-revoked photos (in first keyring only))
		commands=$(gpg --homedir "$gpghome" --with-colons --list-secret-key "${fprs[0]}" | awk '/^pub:/ { if (i>0)  exit } /^(uid|uat):/ { i+=1 }  /^uat:[^r]/ { printf "uid "i"\nrevuid\ny\n4\n\ny\nuid "i"\n" } END { if (i) printf "save\n" }')
		[[ "$commands" ]] || { printf "$FUNCNAME: Error: "$"No editable certificate %s (in %s).""\n" "'$user'" "$gpghome" >&2 ; return 1 ; }

		if [[ "$commands" =~ ^uid ]] ; then
			printf "$FUNCNAME: Info: "$"Revoking previous image(s) inside %s certificate…""\n" "$user" >&2
			gpg --homedir "$gpghome" --batch --command-fd 0 --edit-key "${fprs[0]}" 2> >(grep "image of size" | sort -u >&2) <<<"$commands" || { printf "%s: Error: Can't revoke - good PIN ?.\n" "$FUNCNAME" >&2 ; return 1 ;}
		fi

		if [[ "$image" ]] ; then
			commands=$(printf "addphoto\n${tmpdir}/new.jpg\ny\nsave\n")
			printf "$FUNCNAME: Info: "$"Adding %s into %s certificate…""\n" "$image" "$user" >&2
			gpg --homedir "$gpghome" --batch --command-fd 0 --edit-key "${fprs[0]}" 2> >(grep "image of size" | sort -u >&2 ) <<<"$commands" || { printf "%s: Error: Can't %s - good PIN ?.\n" "$FUNCNAME" "addphoto" >&2 ; return 1 ;}
		fi

		# Send new certificate to keyservers
		for keyserv in ${keyservs[@]} ; do
			gpg --homedir "$gpghome" --keyserver "${keyserv}" --send-keys "${fprs[0]}" || ret=$?
		done
	fi

	if [[ -s "$tmpdir/imglist" ]] ; then
		cat "$tmpdir/imglist"
	else
		printf "%s: Notice: No image extracted from %s certificate...\n" "$FUNCNAME" "$user" >&2
	fi

	return $ret
}

bl_pgpid_email() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [NAME|EMAIL|KEYID|U4|U5]"
	local keyserv keyservs=("${BL_PGPID_KEYSERVERS[@]}")
	local helpmsg="
"$"Display and add or revoke emails inside OpenPGP certificate.""
"$"Missing NAME|EMAIL|KEYID|U4|U5 => Guess it using security token, or interactively ask KEYID of secret key.""
"$"Output usable emails (non-revoked and non-expired).""

OPTIONS:
  -R, --revoke EMAIL          "$"Revoke existing EMAIL (may be used more than once)""
      --revoke-all            "$"Revoke every usable email uid but the newest""
  -A, --add EMAIL             "$"Add EMAIL as a 'Name <EMAIL>' uid. Enable the --name option""
  -y, --yes                   "$"Assume yes: skip the irreversible-revocation confirmation""
  -c, --certs-count           "$"Also output the count of external valid certifications per email (tab-separated). If an email appears in several UIDs, only the maximum count is kept.""
      --show-unusable         "$"Also display the uids that no longer stand: revoked, expired, or without a valid self-signature""
      --info                  "$"Output the emails as pairs key=value ready to be evaluated in bash""
  -v, --verbose               "$"Increase verbosity""
  -H, --homedir GNUPGHOME     "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME .
  -K, --keyservers KEYSERVERS "$"If non-empty, send updated certificate to this keyservers. "$"Default: ""
$(printf -- "%80s\n" ${keyservs[@]})
"
	# TODO: (maybe) sanitize --name or --extra-comment inputs (removing '\' '(' ')' ... ) ?
	local toadd torev pseudo extracomment email certscount=0 assumeyes=0 revokeall=0 showunusable=0 info=0
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local gpgfilter=false
	for ((;$#;)) ; do
		case "$1" in
			-R|--rev|--revoke)
				# Both the bare and the <bracketed> input shapes are accepted.
				shift ; email=${1#<} ; email=${email%>}
				[[ "$email" =~ ^${BL_INTERACTIVE_EMAIL_REGEX}$ ]] || { printf "%s: Error: Invalid email %s.\n" "$FUNCNAME" "'$1'" >&2 ; return 2 ;}
				torev+=("$email") ;;
			-A|--add)
				shift ; email=${1#<} ; email=${email%>}
				[[ "$email" =~ ^${BL_INTERACTIVE_EMAIL_REGEX}$ ]] || { printf "%s: Error: Invalid email %s.\n" "$FUNCNAME" "'$1'" >&2 ; return 2 ;}
				toadd+=("$email") ;;
			--revoke-all|--revokeall) revokeall=1 ;;
			-y|--yes) assumeyes=1 ;;
			-N|--name)
				shift ; pseudo=$1 ;;
			-C|--extracomment|--extra-comment)
				shift ; extracomment=" $1" ;;
			-c|--certs-count|--certscount)
				certscount=1 ;;
			--show-unusable|--showunusable) showunusable=1 ;;
			--info) info=1 ;;
			-k|--keyserver)
				printf -- "$FUNCNAME: Warning: "$"Deprecated option"" '--keyserver' - "$"Please use %s instead.""\n" "'--keyservers'" >&2
				;&
			-K|--keyservers)
				shift ; keyservs=($1) ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-v|--verbose) gpgfilter="cat" ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local user fprs

	user=$(_bl_pgpid_resolve_key "$gpghome" "$1") || return $?

	if [[ "$toadd" ]] ; then
		local uids ids
		# Legacy certificate ? Mint its identity uid first, so the primary can be
		# pinned back on it after the add.
		_bl_pgpid_upgrade_uids "$gpghome" "$user" "$gpgfilter" || return 1
		# Get all uid/uat (from first keyring only)
		uids=$(gpg --homedir "$gpghome" --with-colons --list-secret-key "$user" | awk '/^pub:/ { if (i>0)  exit } /^(uid|uat):/ { i+=1 ; print }')
		[[ "$uids" ]] || { printf "$FUNCNAME: Error: "$"No editable certificate %s (in %s).""\n" "$user" "$gpghome" >&2 ; return 1 ; }

		# Sanity: the certificate must already carry exactly one eid (its own
		# UID:urn:eid:… since the vCard-uid process ; a UID comment before that).
		ids=($(_bl_pgpid_eids_of <<<"$uids"))
		(( ${#ids[@]} > 0 )) || { printf "$FUNCNAME: Error: "$"No eid (u4|u5) in %s certificate.""\n" "$user" >&2 ; return 1 ;}
		(( ${#ids[@]} < 2 )) || { printf "$FUNCNAME: Error: "$"Supernumerary eid (%s).""\n" "${ids[*]}" >&2 ; return 1 ;}
		[[ -z "$extracomment" ]] || printf "$FUNCNAME: Notice: "$"--extra-comment is ignored: a note is carried by its own NOTE: vCard uid""\n" >&2
	fi

	# Plain 'Name <addr>' uid, not a vCard-property one (bl-pgpid-implementation(7)).
	# Keyservers muted here : a single send happens below.
	local uidstr rawu u dup uidname eiduid
	local -a prevuids=() revuids=()
	if ((${#toadd[@]})) ; then
		# The name in front of the address: --name wins, else the certificate's
		# own FN: vCard uid, else (per address) the local part.
		if [[ -z "$pseudo" ]] ; then
			while IFS= read -r rawu ; do
				u=$(printf '%b' "$rawu")
				[[ "$u" =~ ^FN(\;[^:]*)?:\ ?(.*)$ ]] || continue
				pseudo=${BASH_REMATCH[2]} ; break
			done < <(awk -F: '$1=="uid" && $2 ~ /^[ounmfqws-]/ { print $10 }' <<<"$uids")
		fi
		# --with-colons escapes ':' (and '\\', control bytes) as \\xNN in field
		# 10 ; gpg --quick-*-uid need the real UID string back.
		while IFS= read -r rawu ; do prevuids+=("$(printf '%b' "$rawu")") ; done \
			< <(awk -F: '$1=="uid" && $2 ~ /^[ounmfqws-]/ { print $10 }' <<<"$uids")
		while IFS= read -r rawu ; do revuids+=("$(printf '%b' "$rawu")") ; done \
			< <(awk -F: '$1=="uid" && $2 !~ /^[ounmfqws-]/ { print $10 }' <<<"$uids")
		eiduid="UID:urn:eid:${ids[0]}"
	fi
	for email in "${toadd[@]}" ; do
		uidname=${pseudo:-${email%%@*}}
		uidstr="${uidname} <${email}>"
		dup=0
		for u in "${prevuids[@]}" ; do
			[[ "$u" != "$uidstr" ]] || { dup=1 ; break ; }
		done
		((! dup)) || { printf "$FUNCNAME: Notice: "$"%s certificate already carries %s""\n" "$user" "'$uidstr'" >&2 ; continue ; }
		# A revoked twin keeps its string on the certificate forever and gpg
		# refuses an identical one : say so instead of blaming the PIN.
		for u in "${revuids[@]}" ; do
			[[ "$u" != "$uidstr" ]] || { dup=1 ; break ; }
		done
		((! dup)) || { printf "$FUNCNAME: Notice: "$"%s was revoked earlier and cannot be added again — OpenPGP keeps revoked User IDs on the certificate and gpg refuses an identical one.""\n" "'$uidstr'" >&2 ; continue ; }
		printf "%s: Notice: Adding %s into %s certificate...\n" "$FUNCNAME" "'$uidstr'" "$user" >&2
		gpg --homedir "$gpghome" --batch --quick-add-uid "$user" "$uidstr" 2> >( $gpgfilter || grep "^\( *[A-Z«]\|gpg:\)" >&2 ) || { printf "%s: Error: Can't %s - good PIN ?.\n" "$FUNCNAME" "adduid" >&2 ; return 1 ;}
	done
	# One uid revoked per call (re-run to peel the next), deprecated 'EMAIL:'
	# shape first. The keep-one guard counts every email-bearing uid whatever
	# its shape, so the certificate never loses its last address.
	local rawu u revoked=0
	local -a emailuids legacytargets vcardtargets targetuids
	for email in "${torev[@]}" ; do
		# Both the bare and the <bracketed> input shapes are accepted.
		email=${email#<} ; email=${email%>}
		emailuids=() ; legacytargets=() ; vcardtargets=()
		while IFS= read -r rawu ; do
			u=$(printf '%b' "$rawu")
			if [[ "$u" =~ ^EMAIL(\;[^:]*)?:\ ?\<?([^\<\>]+)\>?$ ]] ; then
				emailuids+=("$u")
				[[ "${BASH_REMATCH[2]}" != "$email" ]] || vcardtargets+=("$u")
			elif [[ "$u" =~ \<([^\<\>]+)\>[[:space:]]*$ ]] ; then
				emailuids+=("$u")
				[[ "${BASH_REMATCH[1]}" != "$email" ]] || legacytargets+=("$u")
			fi
		done < <(gpg --homedir "$gpghome" --with-colons --list-secret-key "$user" | awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" && $2 ~ /^[ounmfqws-]/ { print $10 }')
		targetuids=("${vcardtargets[@]}" "${legacytargets[@]}")
		(( ${#targetuids[@]} )) || { printf "$FUNCNAME: Error: "$"No revokable email %s inside %s certificate (in %s).""\n" "'$email'" "$user" "$gpghome" >&2 ; return 1 ; }
		(( ${#emailuids[@]} >= 2 )) || { printf "$FUNCNAME: Error: "$"At least one email must be retained.""\n" >&2 ; return 1 ; }
		_bl_pgpid_revoke_uid "$gpghome" "$user" "${targetuids[0]}" "$gpgfilter" "$assumeyes" || return 1
		revoked=1
	done

	if ((revokeall)) ; then
		local created keep i
		local -a alluids=() allcreated=()
		while IFS=: read -r created rawu ; do
			u=$(printf '%b' "$rawu")
			[[ "$u" =~ ^EMAIL(\;[^:]*)?:\ ?\<?[^\<\>]+\>?$ || "$u" =~ \<[^\<\>]+\>[[:space:]]*$ ]] || continue
			alluids+=("$u") ; allcreated+=("$created")
		done < <(gpg --homedir "$gpghome" --with-colons --list-secret-key "$user" | awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" && $2 ~ /^[ounmfqws-]/ { printf "%s:%s\n", $6, $10 }')
		# Keep the newest (uid self-sig creation date, last listed wins ties).
		keep=-1
		for ((i=0 ; i<${#alluids[@]} ; i++)) ; do
			(( keep >= 0 && allcreated[i] < allcreated[keep] )) || keep=$i
		done
		if (( ${#alluids[@]} < 2 )) ; then
			printf "$FUNCNAME: Warning: "$"Nothing to revoke.""\n" >&2
		else
			for ((i=0 ; i<${#alluids[@]} ; i++)) ; do
				((i != keep)) || continue
				_bl_pgpid_revoke_uid "$gpghome" "$user" "${alluids[i]}" "$gpgfilter" "$assumeyes" || return 1
				revoked=1
			done
		fi
	fi
	if ((revoked)) ; then
		_bl_pgpid_fix_primary "$gpghome" "$user" "$gpgfilter" || return 1
	fi

	local ret=0
	# Send updated certificate to keyservers
	if [[ "$torev" || "$toadd" ]] || ((revokeall)) ; then
		for keyserv in ${keyservs[@]} ; do
			gpg --homedir "$gpghome" --keyserver "${keyserv}" --send-keys "$user" || ret=$?
		done
	fi

	if ((certscount)) ; then
		# --check-sigs (not --list-sigs) so gpg verifies and tags each signature
		# '!' ; local ones (sigclass ending in 'l') are not public certifications
		# and do not count either — bl-pgpid-implementation(7).
		gpg --homedir "$gpghome" --with-colons --check-sigs "$user" 2>/dev/null |
			awk -F: -v owner="${user: -16}" '
				function flush_uid(    e, c) {
					e = cur_email ; c = cur_count
					if (e != "" && (! (e in best) || c > best[e])) best[e] = c
				}
				/^uid:[^re]:/ {
					flush_uid()
					cur_count = 0 ; cur_email = ""
					if (match($10, /<[^>]+>/)) cur_email = substr($10, RSTART + 1, RLENGTH - 2)
					next
				}
				/^(uid|uat):/ {  # revoked/expired uid or uat (photo) — stop counting until next usable uid
					flush_uid()
					cur_count = 0 ; cur_email = ""
					next
				}
				/^sig:!::/ {
					if (cur_email != "" && $5 != owner && $11 !~ /l$/) cur_count++
				}
				END {
					flush_uid()
					for (e in best) printf "%s\t%d\n", e, best[e]
				}' | sort -u
	else
		# The colon listing rather than show-only-fpr-mbox, which cannot tell a
		# usable address from one that no longer stands — and drops the latter
		# silently, so --show-unusable had nothing to show.
		local validity uid addr suffix
		local -A idx=() seen=()
		while IFS=: read -r validity uid ; do
			uid=$(printf '%b' "$uid")
			_bl_pgpid_uid_stands "$validity" || ((showunusable)) || continue
			[[ "$uid" =~ \<([^\<\>]+)\>[[:space:]]*$ ]] || continue
			addr=${BASH_REMATCH[1]}
			suffix="" ; _bl_pgpid_uid_stands "$validity" || suffix="_UNUSABLE"
			[[ -z "${seen[$suffix$addr]}" ]] || continue   # one address, one line per bucket
			seen[$suffix$addr]=.
			if (( ! info )) ; then
				printf '%s\n' "$addr"
				continue
			fi
			printf 'pgpid_EMAIL%s[%d]=%s\n' "$suffix" "${idx[EMAIL$suffix]:-0}" "${addr@Q}"
			idx[EMAIL$suffix]=$(( ${idx[EMAIL$suffix]:-0} + 1 ))
		done < <(gpg --homedir "$gpghome" --with-colons --list-key "$user" 2>/dev/null \
			| awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" { i+=1 ; printf "%s:%s\n", $2, $10 }')
	fi
	return $ret
}

# Which key to work on when the caller gave no target: the one carried by the
# connected security token, else ask. $1 = GNUPGHOME, $2 = card field to
# prefer — pgpid_Skeyfpr, or pgpid_Ckeyfpr where the certification key itself
# is what the caller needs — $3 = prompt for the interactive fallback.
# Does this uid still stand? Field 2 of 'gpg --with-colons' mixes two axes:
# web-of-trust validity (o i n m f u q -) and lifecycle (r e d). We want the
# second, so this is an ALLOW list of the states where the uid holds — anything
# else, including a letter gpg has yet to invent, counts as unusable. A deny
# list would default the other way, and defaulting to "usable" is the dangerous
# side to be wrong on.
#
# 'm' matters: marginal validity says the web of trust vouches for the key only
# weakly, not that the uid is dead. Excluding it hid a third of the addresses on
# a real contact's certificate (measured 2026-08-09).
_bl_pgpid_uid_stands() {
	case "$1" in
		[ounmfqws-]*) return 0 ;;
		*) return 1 ;;
	esac
}

_bl_pgpid_default_key() {
	local gpghome=$1 field=${2:-pgpid_Skeyfpr} prompt=${3:-}
	local -A cardinfo
	bl_pgpid_token_check --homedir "$gpghome" --no-fetch --quiet --aaname cardinfo || true
	if [[ "${cardinfo[$field]}" ]] ; then
		printf '%s' "${cardinfo[$field]}"
	elif [[ "$prompt" ]] ; then
		_bl_pgp_choose_seckeyid --homedir "$gpghome" --text "$prompt"
	else
		_bl_pgp_choose_seckeyid --homedir "$gpghome"
	fi
}

# The same, then resolved to exactly one certificate fingerprint. For the
# actions that need a certificate rather than a key identifier ; the others
# stop at _bl_pgpid_default_key. $1 = GNUPGHOME, $2 = TARGET (may be empty).
_bl_pgpid_resolve_key() {
	local gpghome=$1 user=$2
	[[ "$user" ]] || user=$(_bl_pgpid_default_key "$gpghome") || return $?
	local fprs
	fprs=($(bl_pgpid_get --fingerprint --errexit-g=1 --homedir "$gpghome" --no-fetch -- "$user")) || return $?
	printf '%s' "${fprs[0]}"
}

bl_pgpid_to_vcard() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [NAME|EMAIL|KEYID|U4|U5]"
	local helpmsg="
"$"Convert OpenPGP certificate to vCard (format 4.0).""
"$"Missing NAME|EMAIL|KEYID|U4|U5 => Guess it using security token, or interactively ask KEYID of secret key.""

OPTIONS:
  -o, --output FILE           "$"Write into given FILE instead of standard output""
      --raw                   "$"Don't convert, but raw output all OpenPGP uids strings, separated by empty lines""
  -q, --quiet                 "$"Decrease verbosity""
  -H, --homedir GNUPGHOME     "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME .
"
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local output="" raw=0 quiet=0 target=""
	for ((;$#;)) ; do
		case "$1" in
			-o|--output)   shift ; output=${1:?} ;;
			--raw)         raw=1 ;;
			-q|--quiet)    quiet=1 ;;
			-H|--homedir)  shift ; gpghome=${1:?} ;;
			-h|--h*)       printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version)  printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; [[ -z "$target" ]] && target=${1:-} ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*)  [[ "$target" ]] && printf "$FUNCNAME: Warning: "$"Ignoring extra args '%s'…""\n" "$1" >&2 || target=$1 ;;
		esac
		shift
	done
	local user
	user=$(_bl_pgpid_resolve_key "$gpghome" "$target") || return $?

	# Redirect once, so every branch below just writes to stdout.
	if [[ "$output" ]] ; then
		exec {BL_tmp_fd}>&1 || return 1
		exec >"$output" || { printf "$FUNCNAME: Error: "$"Can't write %s"".\n" "'$output'" >&2 ; return 1 ; }
	fi
	local ret=0
	_bl_pgpid_vcard_render "$gpghome" "$user" "$raw" "$quiet" || ret=$?
	if [[ "$output" ]] ; then
		exec 1>&${BL_tmp_fd} {BL_tmp_fd}>&- || true
		((quiet)) || printf "$FUNCNAME: Notice: "$"Written into %s"".\n" "'$output'" >&2
	fi
	return $ret
}

# The two views to_vcard offers:
#   raw  = every uid string, transposable or not, blank-line separated
#   else = the vCard 4.0 itself: uats + recognized uids
#
# There used to be a third, --info, meant to report the vCard's own content as
# key=value. It could not: the vCard's EMAIL lines come from the 'Name <addr>'
# uids, which are not vCard-property uids, so --info listed the deprecated
# (and here revoked) 'EMAIL:' ones instead — the exact opposite set — and knew
# nothing of KEY or PHOTO. Addresses are 'email''s business ; that is where
# --info now lives.
_bl_pgpid_vcard_render() {
	local gpghome=$1 user=$2 raw=$3 quiet=$4
	local rawu u
	local mono=" FN NOTE "


	if ((raw)) ; then
		while IFS= read -r rawu ; do
			printf '%s\n\n' "$(printf '%b' "$rawu")"
		done < <(gpg --homedir "$gpghome" --with-colons --list-key "$user" 2>/dev/null \
			| awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" { i+=1 ; print $10 }')
		return 0
	fi

	local pref=0 kspref keyurl photofile
	printf 'BEGIN:VCARD\r\nVERSION:4.0\r\n'
	while IFS= read -r rawu ; do
		u=$(printf '%b' "$rawu")
		if [[ "$u" =~ ^([A-Z]+)(\;[^:]*)?:\ ?(.*)$ ]] ; then
			if [[ "${BASH_REMATCH[1]}" == EMAIL ]] ; then
				# Legacy 'EMAIL: <addr>' uid — still rendered so that
				# certificates minted during that experiment keep working.
				u=${BASH_REMATCH[3]} ; u=${u#<} ; u=${u%>}
				pref=$((pref+1)) ; _bl_pgpid_vfold "EMAIL;PREF=${pref}:${u}"
			else
				_bl_pgpid_vfold "$u"                             # already a valid, escaped vCard line
			fi
		elif [[ "$u" =~ \<([^\<\>]+)\>[[:space:]]*$ ]] ; then
			# Plain 'Name <addr>' uid : only its address becomes a vCard
			# line (the name is carried by FN:, the eid by UID:urn:eid:…).
			pref=$((pref+1)) ; _bl_pgpid_vfold "EMAIL;PREF=${pref}:${BASH_REMATCH[1]}"
		fi
	done < <(gpg --homedir "$gpghome" --with-colons --list-key "$user" 2>/dev/null | awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" && $2 ~ /^[ounmfqws-]/ { print $10 }')
	# KEY;MEDIATYPE : retrievable-cert URL from the preferred keyserver
	# (subpacket 24 ; default BL_PGPID_KEYSERVERS[0]).
	kspref=$(bl_pgpid_property ksprefrd --homedir "$gpghome" -- "$user" 2>/dev/null)
	keyurl=$(_bl_pgpid_ks_to_keyurl "${kspref:-${BL_PGPID_KEYSERVERS[0]}}" "${user,,}")   # lowercase fpr, matching pgp2vcard.html
	[[ -z "$keyurl" ]] || _bl_pgpid_vfold "KEY;MEDIATYPE=application/pgp-keys:${keyurl}"
	# KEY inline : minimised export (no third-party sigs, no photo attributes).
	_bl_pgpid_vfold "KEY:data:application/pgp-keys;base64,$(gpg --homedir "$gpghome" --export --export-options export-minimal,no-export-attributes "$user" 2>/dev/null | base64 --wrap=0)"
	# PHOTO : the first UAT image, extracted through gpg's photo-viewer hook.
	photofile=$(mktemp) && {
		gpg --homedir "$gpghome" --photo-viewer "cp %I $photofile" --list-options show-photos --list-key "$user" >/dev/null 2>&1
		[[ ! -s "$photofile" ]] || _bl_pgpid_vfold "PHOTO:data:image/jpeg;base64,$(base64 --wrap=0 "$photofile")"
		rm -f "$photofile"
	}
	printf 'END:VCARD\r\n'
}

bl_pgpid_property() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name PROPERTY [OPTIONS]... [NAME|EMAIL|KEYID|U4|U5]"
	local keyserv keyservs=("${BL_PGPID_KEYSERVERS[@]}")
	local helpmsg="
"$"Display and add or revoke vCard-property uids inside OpenPGP certificate.""
"$"PROPERTY is one of: ""{ name, note, address, phone, url, lang, geo, ksprefrd }.
"$"Email addresses are not vCard-property uids (they keep the 'Name <addr>' shape every mail client understands): manage them with '""$BL_PGPID_NAME email'.
"$"'ksprefrd' is preferred certificate server. This is not stored as a vCard-property uid, but used when generating vCard.""
"$"Missing NAME|EMAIL|KEYID|U4|U5 => Guess it using security token, or interactively ask KEYID of secret key.""
"$"Free-text values (name, note) with , ; \\ or newlines are stored RFC 6350-escaped and decoded back on display (address keeps its structural ';')""

OPTIONS:
  -A, --add VALUE             "$"Add ({name,note,ksprefrd} ⇒ replace) a PROPERTY uid (may be used more than once)""
      --replace-to VALUE      "$"Exact synonym of --add — terminolgy is just more relevant for {name,note,ksprefrd}""
  -R, --revoke VALUE          "$"Revoke the PROPERTY uid carrying VALUE (may be used more than once)""
      --revoke-all            "$"Revoke every usable PROPERTY uid — all but the newest for {name,email}""
  -y, --yes                   "$"Assume yes: skip the irreversible-revocation confirmation""
      --show-unusable         "$"Also display the uids that no longer stand: revoked, expired, or without a valid self-signature""
      --info                  "$"output the PROPERTY values as pairs key=value ready to be evaluated in bash""
  -q, --quiet                 "$"Decrease verbosity""
  -v, --verbose               "$"Increase verbosity""
  -H, --homedir GNUPGHOME     "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME .
  -K, --keyservers KEYSERVERS "$"If non-empty, send updated certificate to this keyservers. "$"Default: ""
$(printf -- "%80s\n" ${keyservs[@]})
"
	# The vCard property (RFC 6350) behind each CLI name. Two deliberate absences:
	# the eid anchor, which the whole web of trust hangs on, and email, which is
	# not a vCard-property uid at all — bl-pgpid-implementation(7).
	local -A vprops=([name]=FN [note]=NOTE [address]=ADR [phone]=TEL [url]=URL [lang]=LANG [geo]=GEO)
	# House cardinality (stricter than RFC 6350) : one usable {FN,NOTE} at a
	# time (a new one revokes the previous), at least one usable FN kept.
	local mono=" FN NOTE " keepone=" FN "
	local -a toadd=() torev=()
	local revokeall=0 showunusable=0 info=0 quiet=0 assumeyes=0 prop vprop target
	local gpghome=${GNUPGHOME:-~/.gnupg}
	local gpgfilter=false
	for ((;$#;)) ; do
		case "$1" in
			-A|--add|--replace-to)
				shift ; toadd+=("${1:?}") ;;
			-R|--rev|--revoke)
				shift ; torev+=("${1:?}") ;;
			--revoke-all|--revokeall) revokeall=1 ;;
			-y|--yes) assumeyes=1 ;;
			--show-unusable|--showunusable) showunusable=1 ;;
			--info) info=1 ;;
			-q|--quiet) quiet=1 ;;
			-K|--keyservers)
				shift ; keyservs=($1) ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-v|--verbose) gpgfilter="cat" ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift
				while (($#)) ; do
					if [[ -z "$prop" ]] ; then prop=$1
					elif [[ -z "$target" ]] ; then target=$1
					else printf "$FUNCNAME: Warning: "$"Ignoring extra args '%s'…""\n" "$1" >&2
					fi
					shift
				done ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*)	if [[ -z "$prop" ]] ; then prop=$1
				elif [[ -z "$target" ]] ; then target=$1
				else printf "$FUNCNAME: Warning: "$"Ignoring extra args '%s'…""\n" "$1" >&2
				fi ;;
		esac
		shift
	done

	# One action, one property: PROPERTY is always required now. The views
	# that span the whole certificate moved to '$BL_PGPID_NAME to_vcard'.
	[[ "$prop" ]] || { printf "$FUNCNAME: Error: "$"Missing PROPERTY argument"".\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ; }
	if [[ "${prop,,}" == ksprefrd ]] ; then
		vprop=KSPREFRD	# not a vCard uid : the « preferred keyserver » subpacket (24), handled apart below
	else
		vprop=${vprops[${prop,,}]}
		[[ "$vprop" ]] || { printf "$FUNCNAME: Error: "$"Unrecognized property"" '%s'.\n\n"$"Try '%s --help' for more information"".\n" "$prop" "$name" >&2 ; return 2 ; }
	fi

	(( ${#toadd[@]} < 2 )) || [[ "$mono" != *" $vprop "* ]] || { printf "$FUNCNAME: Error: "$"Only one --add at a time for {name,note} (the new value revokes the previous one)"".\n" >&2 ; return 2 ; }

	# Sanity checks before minting a uid : strict on the machine-parseable
	# properties, light (non-empty, no control character) on the free-text ones.
	local telre='^(tel:)?\+[0-9][0-9 ./()-]{3,22}[0-9]$'
	local urlre='^[a-zA-Z][a-zA-Z0-9+.-]*://[^[:space:]]+$'
	local langre='^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{1,8})*$'
	local geore='^geo:-?[0-9]{1,2}(\.[0-9]+)?,-?[0-9]{1,3}(\.[0-9]+)?(,-?[0-9]+(\.[0-9]+)?)?(;(u|crs)=[a-zA-Z0-9.-]+)*$'
	local val
	# vCard text escaping (RFC 6350 §3.4). Free-text values (FN, NOTE) escape , ; \
	# and a newline ; ADR keeps its STRUCTURAL ';' separators ; URI / tag values
	# (TEL, URL, GEO, LANG, EMAIL) stay verbatim. --revoke gets the same encoding
	# so it matches the stored form ; display decodes it (--raw keeps it).
	local escprof="" keepsemi=""
	case "$vprop" in FN|NOTE) escprof=1 ;; ADR) escprof=1 ; keepsemi=1 ;; esac
	if [[ "$escprof" ]] ; then
		local i
		for i in "${!toadd[@]}" ; do toadd[i]=$(_bl_pgpid_vcard_esc "${toadd[i]}" "$keepsemi") ; done
		for i in "${!torev[@]}" ; do torev[i]=$(_bl_pgpid_vcard_esc "${torev[i]}" "$keepsemi") ; done
	fi
	for val in "${toadd[@]}" ; do
		if [[ -z "$val" ]] || [[ "$val" =~ [[:cntrl:]] ]] ; then
			printf "$FUNCNAME: Error: "$"Invalid %s value"" %s.\n" "$vprop" "'$val'" >&2 ; return 2
		fi
		case "$vprop" in
			EMAIL) [[ "$val" =~ ^${BL_INTERACTIVE_EMAIL_REGEX}$ ]] ;;
			TEL)   [[ "$val" =~ $telre ]] ;;
			URL)   [[ "$val" =~ $urlre ]] ;;
			LANG)  [[ "$val" =~ $langre ]] ;;
			GEO)   [[ "$val" =~ $geore ]] ;;
			*) true ;;
		esac || { printf "$FUNCNAME: Error: "$"Invalid %s value"" %s.\n" "$vprop" "'$val'" >&2 ; return 2 ; }
	done

	local user
	user=$(_bl_pgpid_resolve_key "$gpghome" "$target") || return $?

	local ret=0 modified=0


	# 'ksprefrd' : the preferred keyserver (OpenPGP self-sig subpacket 24). Not a
	# vCard-property uid — it lives on the primary uid's self-sig and only shapes
	# the KEY;MEDIATYPE URL of the generated vCard. Read via gpg's authoritative
	# 'showpref' (newest self-sig), written via 'edit-key … keyserver' on the
	# primary uid (the uid gpg lists first). --revoke is meaningless here, and
	# --show-unusable leaves it out (it is no uid).
	if [[ "$vprop" == KSPREFRD ]] ; then
		(( ${#torev[@]} + revokeall == 0 )) || { printf "$FUNCNAME: Error: "$"'ksprefrd' cannot be revoked.""\n" >&2 ; return 2 ; }
		local primaryuid
		primaryuid=$(gpg --homedir "$gpghome" --with-colons --list-key "$user" 2>/dev/null | awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" && $2 ~ /^[ounmfqws-]/ { print $10 ; exit }')
		primaryuid=$(printf '%b' "$primaryuid")
		[[ "$primaryuid" ]] || { printf "$FUNCNAME: Error: "$"No usable primary uid in %s.""\n" "$user" >&2 ; return 1 ; }
		if (( ${#toadd[@]} )) ; then
			(( ${#toadd[@]} == 1 )) || { printf "$FUNCNAME: Error: "$"Only one --add at a time for 'ksprefrd' (it replaces the previous value)"".\n" >&2 ; return 2 ; }
			[[ "${toadd[0]}" =~ ^hkps?:// ]] || { printf "$FUNCNAME: Error: "$"'ksprefrd' must start with hkp(s)://"" (%s).\n" "'${toadd[0]}'" >&2 ; return 2 ; }
			# One edit-key session on the primary uid. 'uid 1' is an INDEX, never
			# a string (a string matches nothing, and the keyserver would then
			# spill onto EVERY uid). 'primary' must be re-asserted in the SAME
			# self-sig : the keyserver re-sign drops subpacket 25 otherwise.
			# A card-held key re-signs here → PIN.
			printf 'uid 1\nkeyserver\n%s\ny\nprimary\nsave\n' "${toadd[0]}" \
				| gpg --homedir "$gpghome" --batch --command-fd 0 --edit-key "$user" 2> >( $gpgfilter || grep "^\(\[\|gpg:\)" >&2 ) \
				|| { printf "$FUNCNAME: Error: "$"Can't set preferred keyserver - good PIN ?.""\n" >&2 ; return 1 ; }
			for keyserv in ${keyservs[@]} ; do
				gpg --homedir "$gpghome" --keyserver "$keyserv" --send-keys "$user" || ret=$?
			done
		else
			# Preferred keyserver (subpacket 24) from each non-revoked uid's newest
			# binding self-sig, read from the packet dump ('showpref' is too
			# state-dependent to script). The awk emits "<is-primary>\t<keyserver>" ;
			# keep the primary uid's value, failing that the last non-revoked one.
			local kspref primk="" lastk="" ip kv
			local -A seen=()
			while IFS=$'\t' read -r ip kv ; do
				[[ "$kv" ]] || continue
				((ip)) && primk=$kv ; lastk=$kv ; seen[$kv]=1
			done < <(gpg --homedir "$gpghome" --export "$user" 2>/dev/null | gpg --homedir "$gpghome" --list-packets 2>/dev/null | awk -v owner="${user: -16}" '
				function fs() { if (!inuid || !self) return ; if (sc ~ /^0x1[0-3]$/) { if (cr>mb) { mb=cr ; mbks=sks ; mbp=sp } } else if (sc=="0x30") { if (cr>mr) mr=cr } }
				function fu() { fs() ; if (inuid && mb>0 && mb>mr && mbks!="") print mbp "\t" mbks }
				/^:public subkey packet:|^:secret subkey packet:|^:public key packet:/ { fu() ; inuid=0 ; next }
				/^:user ID packet:/ { fu() ; inuid=1 ; mb=0 ; mr=0 ; mbks="" ; mbp=0 ; self=0 ; cr=0 ; sks="" ; sp=0 ; sc="" ; next }
				/^:signature packet:/ { fs() ; self=($0 ~ owner) ; cr=0 ; sks="" ; sp=0 ; sc="" ; next }
				/, created / { for (i=1;i<=NF;i++) { if ($i=="created") cr=$(i+1)+0 ; if ($i=="sigclass") sc=$(i+1) } }
				/preferred keyserver:/ { s=$0 ; sub(/.*preferred keyserver: /,"",s) ; sub(/\)$/,"",s) ; sks=s }
				/primary user ID/ { sp=1 }
				END { fu() }')
			kspref=${primk:-$lastk}
			(( ${#seen[@]} < 2 )) || printf "$FUNCNAME: Warning: "$"Several preferred keyservers across uids (%s) — kept '%s'.""\n" "${!seen[*]}" "$kspref" >&2
			((info)) && printf 'pgpid_ksprefrd=%s\n' "${kspref@Q}" || printf '%s\n' "$kspref"
		fi
		return $ret
	fi

	if (( ${#toadd[@]} + ${#torev[@]} + revokeall )) ; then
		local uids uidstr rawu u v dup created keep i eiduid firstuid
		local -a prevuids revuids curuids matched curcreated
		# Legacy certificate ? Mint its vCard-uid shape first (identity uid and
		# FN:, legacy uids kept), so the operations below see it.
		_bl_pgpid_upgrade_uids "$gpghome" "$user" "$gpgfilter" || return 1
		# Get all uid/uat (from first keyring only)
		uids=$(gpg --homedir "$gpghome" --with-colons --list-secret-key "$user" | awk '/^pub:/ { if (i>0)  exit } /^(uid|uat):/ { i+=1 ; print }')
		[[ "$uids" ]] || { printf "$FUNCNAME: Error: "$"No editable certificate %s (in %s).""\n" "$user" "$gpghome" >&2 ; return 1 ; }

		# Usable uids of this property (decoded), before any change.
		prevuids=()
		while IFS= read -r rawu ; do
			# --with-colons escapes ':' (and '\\', control bytes) as \\xNN in
			# field 10 ; gpg --quick-*-uid need the real UID string back.
			u=$(printf '%b' "$rawu")
			[[ ! "$u" =~ ^${vprop}(\;[^:]*)?:\ ?(.*)$ ]] || prevuids+=("$u")
		done < <(awk -F: '$1=="uid" && $2 ~ /^[ounmfqws-]/ { print $10 }' <<<"$uids")
		# Revoked/expired twins of this property : gpg refuses to re-add a uid
		# string identical to one already on the key (revocation keeps it there
		# forever), so a match here is unaddable and gets its own clear message.
		revuids=()
		while IFS= read -r rawu ; do
			u=$(printf '%b' "$rawu")
			[[ ! "$u" =~ ^${vprop}(\;[^:]*)?:\ ?(.*)$ ]] || revuids+=("$u")
		done < <(awk -F: '$1=="uid" && $2 !~ /^[ounmfqws-]/ { print $10 }' <<<"$uids")

		for val in "${toadd[@]}" ; do
			uidstr="${vprop}:${val}"
			dup=0
			for u in "${prevuids[@]}" ; do
				[[ "$u" != "$uidstr" ]] || { dup=1 ; break ; }
			done
			((! dup)) || { printf "$FUNCNAME: Notice: "$"%s certificate already carries %s""\n" "$user" "'$uidstr'" >&2 ; continue ; }
			for u in "${revuids[@]}" ; do
				[[ "$u" != "$uidstr" ]] || { dup=1 ; break ; }
			done
			((! dup)) || { printf "$FUNCNAME: Notice: "$"%s was revoked earlier and cannot be added again — OpenPGP keeps revoked User IDs on the certificate and gpg refuses an identical one.""\n" "'$uidstr'" >&2 ; continue ; }
			printf "%s: Notice: Adding %s into %s certificate...\n" "$FUNCNAME" "'$uidstr'" "$user" >&2
			gpg --homedir "$gpghome" --batch --quick-add-uid "$user" "$uidstr" 2> >( $gpgfilter || grep "^\( *[A-Z«]\|gpg:\)" >&2 ) || { printf "%s: Error: Can't %s - good PIN ?.\n" "$FUNCNAME" "adduid" >&2 ; return 1 ;}
			modified=1
		done

		# House cardinality : the {FN,NOTE} uids that were usable before this
		# --add get revoked now (add first, revoke after, so the property is
		# never left without any value).
		if ((modified)) && [[ "$mono" == *" $vprop "* ]] ; then
			for u in "${prevuids[@]}" ; do
				_bl_pgpid_revoke_uid "$gpghome" "$user" "$u" "$gpgfilter" "$assumeyes" || return 1
			done
		fi

		for val in "${torev[@]}" ; do
			# Usable uids of the property right now (--add above included).
			curuids=() ; matched=()
			while IFS= read -r rawu ; do
				u=$(printf '%b' "$rawu")
				[[ ! "$u" =~ ^${vprop}(\;[^:]*)?:\ ?(.*)$ ]] || curuids+=("$u")
			done < <(gpg --homedir "$gpghome" --with-colons --list-secret-key "$user" | awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" && $2 ~ /^[ounmfqws-]/ { print $10 }')
			# VALUE matches the parsed value (vCard parameters ignored).
			v=$val
			for u in "${curuids[@]}" ; do
				[[ "$u" =~ ^${vprop}(\;[^:]*)?:\ ?(.*)$ ]] || continue
				[[ ${BASH_REMATCH[2]} != "$v" ]] || matched+=("$u")
			done
			(( ${#matched[@]} > 0 )) || { printf "$FUNCNAME: Error: "$"No revokable %s inside certificate %s (in %s).""\n" "'${vprop}:${val}'" "$user" "$gpghome" >&2 ; return 1 ; }
			if [[ "$keepone" == *" $vprop "* ]] && (( ${#curuids[@]} - ${#matched[@]} < 1 )) ; then
				printf "$FUNCNAME: Error: "$"At least one %s must be retained.""\n" "$vprop" >&2 ; return 1
			fi
			for u in "${matched[@]}" ; do
				_bl_pgpid_revoke_uid "$gpghome" "$user" "$u" "$gpgfilter" "$assumeyes" || return 1
			done
			modified=1
		done

		if ((revokeall)) ; then
			curuids=() ; curcreated=()
			while IFS=: read -r created rawu ; do
				u=$(printf '%b' "$rawu")
				[[ ! "$u" =~ ^${vprop}(\;[^:]*)?:\ ?(.*)$ ]] || { curuids+=("$u") ; curcreated+=("$created") ; }
			done < <(gpg --homedir "$gpghome" --with-colons --list-secret-key "$user" | awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" && $2 ~ /^[ounmfqws-]/ { printf "%s:%s\n", $6, $10 }')
			keep=-1
			if [[ "$keepone" == *" $vprop "* ]] ; then
				# All but the newest (uid self-sig creation, last listed wins ties).
				for ((i=0 ; i<${#curuids[@]} ; i++)) ; do
					(( keep >= 0 && curcreated[i] < curcreated[keep] )) || keep=$i
				done
			fi
			if (( ${#curuids[@]} - (keep >= 0 ? 1 : 0) < 1 )) ; then
				printf "$FUNCNAME: Warning: "$"Nothing to revoke.""\n" >&2
			else
				for ((i=0 ; i<${#curuids[@]} ; i++)) ; do
					(( i != keep )) || continue
					_bl_pgpid_revoke_uid "$gpghome" "$user" "${curuids[i]}" "$gpgfilter" "$assumeyes" || return 1
				done
				modified=1
			fi
		fi

		if ((modified)) ; then
			# The primary flag is left strictly alone. It is, by convention,
			# the holder's main email address — not an identity anchor — and
			# a property uid has no business moving it.

			# Send updated certificate to keyservers
			for keyserv in ${keyservs[@]} ; do
				gpg --homedir "$gpghome" --keyserver "${keyserv}" --send-keys "$user" || ret=$?
			done
		fi
	fi

	# Display (the modified state, when modification options were given).
	local -a ulines
	# ':' as inner separator : raw field 10 never carries one (escaped \x3a),
	# and unlike a tab it keeps empty fields apart (revoked uids have no $6).
	readarray -t ulines < <(gpg --homedir "$gpghome" --with-colons --list-key "$user" 2>/dev/null \
		| awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" { i+=1 ; printf "%s:%s:%s\n", $2, $6, $10 }')
	local line validity created uid P value suffix
	local -A idx=()
	for line in "${ulines[@]}" ; do
		IFS=: read -r validity created uid <<<"$line"
		uid=$(printf '%b' "$uid")
		_bl_pgpid_uid_stands "$validity" || ((showunusable)) || continue
		# A vCard-property uid : NAME[;PARAMS]:[ ]VALUE - parameters possibly
		# stamped by another program are ignored (kept under --raw only).
		if [[ "$uid" =~ ^([A-Z]+)(\;[^:]*)?:\ ?(.*)$ ]] ; then
			P=${BASH_REMATCH[1]} ; value=${BASH_REMATCH[3]}
		else
			P=""
		fi
		[[ "$P" == "$vprop" ]] || continue
		case "$P" in
			FN|NOTE|ADR)
				# RFC 6350 unescape — a sentinel keeps '\\' clear of the other passes.
				value=${value//'\\'/$'\x01'} ; value=${value//'\n'/$'\n'} ; value=${value//'\,'/,}
				[[ "$P" == ADR ]] || value=${value//'\;'/;}   # ADR ';' are structural, never escaped
				value=${value//$'\x01'/\\}
				;;
		esac
		# Plain values by default — one per line, for the eye and for any
		# caller that is not bash. A multi-line note prints as it stands.
		# '--info' is what makes the output parseable, and it alone marks
		# the revoked and expired ones apart.
		if (( ! info )) ; then
			printf '%s\n' "$value" ; continue
		fi
		suffix="" ; _bl_pgpid_uid_stands "$validity" || suffix="_UNUSABLE"
		if [[ -z "$suffix" ]] && [[ "$mono" == *" $P "* ]] ; then
			printf 'pgpid_%s=%s\n' "$P" "${value@Q}"
		else
			printf 'pgpid_%s[%d]=%s\n' "$P$suffix" "${idx[$P$suffix]:-0}" "${value@Q}"
			idx[$P$suffix]=$(( ${idx[$P$suffix]:-0} + 1 ))
		fi
	done

	return $ret
}

bl_pgpid_token_check()
{
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]..."
	local helpmsg="
"$"Check if security token is correctly configured for OpenPGP ID ; may output informations.""
"$"Will try to import cleaned certificate indicated in 'URL of public key' field.""

OPTIONS:
  -f, --no-fetch          "$"Don't try to fetch public certificate (from URL indicated in token metadata)""
  -q, --quiet             "$"Don't errput 'Info' or 'Notice' messages""
  -p, --cert-fpr          "$"Output certificate fingerprint (Certification key fpr)""
  -i, --info              "$"Output content of the associative array containing relevant metadata (may be evaluted with eval)""
  -A, --aaname VARNAME    "$"If VARNAME is a reacheable associative array: fill it (whithout cleaning known fields)""
                            "$"else: Output 'declare -A VARNAME=…'""
  -H, --homedir GNUPGHOME "$"GnuPG home directory (default: ~/.gnupg). "$"Environnement variable: ""\$GNUPGHOME

"$"Return value:""
-   "$"0 if no error and security token is correctly configured for OpenPGP ID.""
- "$"100 + number of missing OpenPGP ID data fields.""
- "$"then 107 if all required data are missing (OpenPGP card is probably empty).""
- "$"Other non-zero on other errors.""
"
	local varname fetch=1 info=0 quiet=0 isaa=0 noCfpr=1
	local gpghome=${GNUPGHOME:-~/.gnupg}
	for ((;$#;)) ; do
		case "$1" in
			-f|--no-fetch) fetch=0 ;;
			-p|--cert-fpr) noCfpr=0 ;;
			-i|--info) info=1 ;;
			-q|--quiet) quiet=1 ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-A|--aaname)
				shift
				varname=${1:?}
				if [[ "$(eval echo \${$varname@a})" =~ A ]] ; then
					local -n aaname=$varname
					isaa=1
				fi
				;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	((! $#)) || printf "$FUNCNAME: Warning: "$"Ignoring extra args '%s'…""\n" "$1" >&2

	[[ "${aaname@a}" ]] || local -A aaname

	# Create gpghome if missing (workaround for Gnupg's bug : sometime ~/.gnupg is created (-K or -k), sometime not (--card-status))
	[[ -d "$gpghome" ]] || ( mkdir -p "$gpghome" && chmod go-rwx "$gpghome" ) || return $?

	local cardstatus
	cardstatus=$(LANG=C.UTF-8 gpg --homedir "$gpghome" --card-status) || return $?

	# Try Import/Refresh Key from URL metadata (as email may be in public certificate only)
	aaname[pgpid_certurl]=$(sed -n ' /^URL/ { s,[^:]*: ,,p ; q }' <<<"$cardstatus")
	[[ "${aaname[pgpid_certurl]}" != "[not set]" ]] || aaname[pgpid_certurl]=""
	if [[ "${aaname[pgpid_certurl]}" ]] && ((fetch)) ; then
		((quiet)) || printf "$FUNCNAME: Notice: "$"Getting pubkey from %s …""\n" "${aaname[pgpid_certurl]}" >&2
		if curl --no-progress-meter "${aaname[pgpid_certurl]}" | gpg --homedir "$gpghome" --import ; then
			cardstatus=$(LANG=C.UTF-8 gpg --homedir "$gpghome" --card-status) || return $?
		fi
	fi

	local line emails ids ttype tversion manufacturer sn

	while read line ; do
		if [[ "$line" =~ ^Application\ ID ]] ; then
			aaname[token_ID]="${line#*: }"
		elif [[ "$line" =~ ^Application\ type ]] ; then
			ttype="${line#*: }"
		elif [[ "$line" =~ ^Version ]] ; then
			tversion="${line#*: }"
		elif [[ "$line" =~ ^Manufacturer ]] ; then
			manufacturer="${line#*: }"
		elif [[ "$line" =~ ^Serial\ number ]] ; then
			sn="${line#*: }"
		elif [[ "$line" =~ ^Name\ of\ cardholder ]] ; then
			aaname[pgpid_name]="${line#*: }"
			[[ "${aaname[pgpid_name]}" != "[not set]" ]] || aaname[pgpid_name]=""
			if [[ "${aaname[pgpid_name]}" =~ ^${BL_INTERACTIVE_EMAIL_REGEX}$ ]] ; then
				# Only first part of email should be relevant
				aaname[pgpid_name]=${BASH_REMATCH[1]}
			else
				# remove first eventual email
				aaname[pgpid_name]=$(sed --regexp-extended "s,${BL_INTERACTIVE_EMAIL_REGEX},, ; s, *$,, ; s,^ *,," <<<"${aaname[pgpid_name]}")
			fi
		elif [[ "$line" =~ ^Signature\ key ]] ; then
			aaname[pgpid_Skeyfpr]=$(sed --regexp-extended --silent ' { s, ,,g ; s,.*([ABCDEF0-9]{40}).*,\1,p } ' <<<"${line}")
		elif [[ "$line" =~ ^Encryption\ key ]] ; then
			aaname[pgpid_Ekeyfpr]=$(sed --regexp-extended --silent ' { s, ,,g ; s,.*([ABCDEF0-9]{40}).*,\1,p } ' <<<"${line}")
		elif [[ "$line" =~ ^Authentication\ key ]] ; then
			aaname[pgpid_Akeyfpr]=$(sed --regexp-extended --silent ' { s, ,,g ; s,.*([ABCDEF0-9]{40}).*,\1,p } ' <<<"${line}")
		fi

		if [[ "$line" =~ ${BL_INTERACTIVE_EMAIL_REGEX} ]] ; then
			grep --quiet "\<${BASH_REMATCH[0]}\>" <<<"${emails[@]}" || emails+=("${BASH_REMATCH[0]}")
		fi
	done <<<"${cardstatus}"

	# Signature key on token may differs from main key, aka Certification key.
	aaname[pgpid_Ckeyfpr]=$(gpg --homedir "$gpghome" --list-options show-only-fpr-mbox --list-key "${aaname[pgpid_Skeyfpr]:- ${aaname[pgpid_Ekeyfpr]:- ${aaname[pgpid_Akeyfpr]}}}" | grep -o -m1 "\<[0-9ABCDEF]\{40\}\>") || echo "$FUNCNAME: Warning:" $"No known certification key. Please share or get certificate (keyserver, by email, ...)" >&2
	((noCfpr)) || echo "${aaname[pgpid_Ckeyfpr]}"

	# Legacy certificate ? Mint its vCard-uid shape (the connected token holds
	# the secret key). Skipped in --quiet mode : a metadata check must not
	# unexpectedly ask for a PIN.
	if ((! quiet)) && [[ "${aaname[pgpid_Ckeyfpr]}" ]] ; then
		_bl_pgpid_upgrade_uids "$gpghome" "${aaname[pgpid_Ckeyfpr]}" || true
	fi

	# The name now lives in the certificate's FN: uid (vCard-uid process) ; it
	# supersedes the limited « Name of cardholder » (DO 5B) read from the token.
	# Read from the local keyring only — offline-safe, no PIN ; when the cert is
	# absent (no network, not fetched) the DO 5B name is kept as a best-effort
	# fallback.
	if [[ "${aaname[pgpid_Ckeyfpr]}" ]] ; then
		local rawu fn=""
		while IFS= read -r rawu ; do
			rawu=$(printf '%b' "$rawu")
			[[ "$rawu" =~ ^FN(\;[^:]*)?:\ ?(.*)$ ]] || continue
			fn=${BASH_REMATCH[2]}
			# RFC 6350 unescape (a sentinel keeps '\\' clear of the other passes).
			fn=${fn//'\\'/$'\x01'} ; fn=${fn//'\n'/$'\n'} ; fn=${fn//'\,'/,} ; fn=${fn//'\;'/;} ; fn=${fn//$'\x01'/\\}
			break
		done < <(gpg --homedir "$gpghome" --with-colons --list-key "${aaname[pgpid_Ckeyfpr]}" 2>/dev/null | awk -F: '/^pub:/ { if (i>0) exit } $1=="uid" && $2 ~ /^[ounmfqws-]/ { print $10 }')
		[[ -z "$fn" ]] || aaname[pgpid_name]=$fn
	fi

	aaname[pgpid_email]="${emails[0]}"
	(( ${#emails[@]} < 2 )) || ((quiet)) || printf "$FUNCNAME: Notice: "$"Supernumerary emails (%d), skipping %s …""\n" ${#emails[@]} "${emails[@]:1}" >&2

	# Should be in 'Login data' field, but who knows ? (yet...) 
	ids=($(sed --silent --regexp-extended "s,.*4.?(${BL_PGPID_U4_REGEX}).*,u4\1,p ; s,.*5.?(${BL_PGPID_U5_REGEX}).*,u5\1,p" <<<"${cardstatus}" | sort -u))
	(( ${#ids[@]} < 2 )) || { printf "$FUNCNAME: Error: "$"Supernumerary eid (%s).""\n" "${ids[*]}" >&2 ; return 1 ;}
	aaname[pgpid_id]="${ids[0]}"

	aaname[token_AVersion]="${ttype} ${tversion}"
	aaname[token_MSN]="${manufacturer} ${sn}"

	local key ret=0
	if ((info)) ; then
		echo >&2
		for key in "${!aaname[@]}" ; do
			echo "${key}=${aaname[$key]@Q}"
		done
		echo >&2
	fi

	if ((!isaa)) && [[ "$varname" ]] ; then
	   declare -p aaname | sed "s, aaname=, $varname=,"
	fi

	for key in pgpid_name pgpid_id pgpid_email pgpid_Skeyfpr pgpid_Ekeyfpr pgpid_Akeyfpr pgpid_certurl ; do
		if [[ -z "${aaname[$key]}" ]] ; then
			((ret = ret ? ret+1 : 101))
			((quiet)) || printf "$FUNCNAME: Notice: "$"Invalid OpenPGP ID token, missing:"" '${key}'.\n" >&2
		fi
	done

	! ((ret)) || return $ret

	((quiet)) || printf "$FUNCNAME: Info: "$"Valid OpenPGP ID token"", pgpid_Ckeyfpr='${aaname[pgpid_Ckeyfpr]}', pgpid_id='${aaname[pgpid_id]}'.\n" >&2
	return 0
}

bl_pgpid_cert_check() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [NAME|EID|EMAIL|FPR]..."
	local helpmsg="
"$"Check WoT-validity of certificate(s) identified by NAME|EID|EMAIL|FPR (or all if none).""

"$"Before that you may run:""
$ $BL_PGPID_NAME get --recurse …    # "$"To populate the keyring""
$ $BL_PGPID_NAME update_trustdb …   # "$"To set your Web-of-Trust configuration""

"$"Default output is one line per certificate: FINGERPRINT  EID  VERDICT""
"$"VERDICT's possible values:"" certified | uncertified | revoked | broken""

"$"A certificate is 'broken' when its non-revoked uids carry conflicting or no entity
identifier (EID), or when no presentable uid carries an email.""

OPTIONS:
  -E, --show-email         "$"Insert the email column #2 (one line per presentable uid)""
  -c, --certs-count        "$"Insert, before VERDICT, the count of distinct external certifiers (valid non-self signatures over all uids, one signer counted once)""
  -L, --no-check-eid       "$"Legacy: don't require a consistent eid (certs not yet OpenPGP-ID aware)""
  -H, --homedir GNUPGHOME  "$"GnuPG home to read keys & trust from (default: ~/.gnupg)""
  -q, --quiet              "$"No per-line output. Exit code hold verdict: 0 if all certified, 196 if any uncertified, 197 if any revoked, 198 if any broken.""
"
	local syshome=${GNUPGHOME:-~/.gnupg} quiet=0 showmail=0 certscount=0 checkeid=1
	for ((;$#;)) ; do
		case "$1" in
			-E|--show-email)   showmail=1 ;;
			-c|--certs-count|--certscount) certscount=1 ;;
			-L|--no-check-eid) checkeid=0 ;;
			-H|--homedir) shift ; syshome=${1:?} ;;
			-q|--quiet)   quiet=1 ;;
			-h|--help)    printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '%s'\n" "$1" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done
	local -a targets=("$@")

	# Pure read: query the trust exactly as it currently stands in the real homedir —
	# no throwaway copy, no import, no --check-trustdb. Extending the trust (and the
	# only mutating step) belongs to '$BL_PGPID_NAME update_trustdb'. Local signatures,
	# if any, count as GnuPG sees them.
	local -a GPG=(gpg --homedir "$syshome" --quiet --batch)

	# Resolve targets → primary fingerprints, deduplicated. Includes certs with no
	# emailed uid (so a 'broken' one is still reported).
	local -a fprs
	readarray -t fprs < <("${GPG[@]}" --with-colons --list-keys -- "${targets[@]}" 2>/dev/null \
		| awk -F: '$1=="pub"{p=1} $1=="fpr"&&p{print $10;p=0}' | sort -u)
	(( ${#fprs[@]} > 0 )) || { printf "$FUNCNAME: Error: "$"No certificate for"" '%s'...\n" "$targets" >&2 ; return 141 ; }

	# Per-certificate verdict → severity feeding the --quiet exit code:
	# broken=198 dominates revoked=197 dominates uncertified=196 dominates certified=0.
	local F colon eidfield verdict sev nNR cl rc=0 ncerts ccol=""
	local -a nreids fueids mbox
	for F in "${fprs[@]}" ; do
		colon=$("${GPG[@]}" --with-colons --list-keys "0x$F" 2>/dev/null)
		readarray -t nreids < <(_bl_pgpid_eids_of '[^r]' <<<"$colon")   # eids on non-revoked uids
		readarray -t fueids < <(_bl_pgpid_eids_of '[fu]'  <<<"$colon")  # eids on fully-valid uids
		nNR=${#nreids[@]}
		# Display lines: one per emailed uid GnuPG deems presentable (validity ∉ i/d/r/e).
		readarray -t mbox < <("${GPG[@]}" --list-options show-only-fpr-mbox --list-keys "0x$F" 2>/dev/null | sort -u)

		# Field #3 : the single eid the non-revoked uids agree on, else '-'.
		((nNR==1)) && eidfield="${nreids[0]}" || eidfield="-"

		# Field #4 : the verdict.
		if (( ${#mbox[@]} == 0 )) ; then
			verdict=broken ; eidfield="-"                                # no live emailed uid (always fatal)
		elif ((checkeid)) && ((nNR>=2)) ; then
			verdict=broken ; eidfield="-"                                # conflicting eids on non-revoked uids
		elif ((checkeid)) && ((nNR==0)) ; then
			verdict=broken ; eidfield="-"                                # no eid at all
		elif grep --quiet -m1 "^pub:r" <<<"$colon" ; then
			verdict=revoked
		elif ((checkeid)) ; then
			# nNR==1 : certified iff that eid is borne by a fully-valid (f/u) uid.
			[[ " ${fueids[*]} " == *" ${nreids[0]} "* ]] && verdict=certified || verdict=uncertified
		else
			# --no-check-eid : certified if any fully-valid (f/u) uid exists.
			[[ "$(awk -F: '$1=="uid" && ($2=="f"||$2=="u"){print 1;exit}' <<<"$colon")" ]] \
				&& verdict=certified || verdict=uncertified
		fi

		case $verdict in broken) sev=198 ;; revoked) sev=197 ;; uncertified) sev=196 ;; *) sev=0 ;; esac
		((sev<=rc)) || rc=$sev

		((quiet)) || {
			# Optional column, before VERDICT : distinct external certifiers, ie.
			# valid ('!') signatures over all uid/uat, deduped on the signer keyid.
			# Same counting rules as 'email --certs-count' — see
			# bl-pgpid-implementation(7).
			if ((certscount)) ; then
				ncerts=$("${GPG[@]}" --with-colons --check-sigs "0x$F" 2>/dev/null \
					| awk -F: -v owner="${F: -16}" '
						$1=="uid" || $1=="uat" { inuid=1 ; next }
						$1=="pub" || $1=="sub" { inuid=0 ; next }
						inuid && $1=="sig" && $2=="!" && $5!=owner && $11 !~ /l$/ { seen[$5]=1 }
						END { n=0 ; for (s in seen) n++ ; print n }')
				printf -v ccol -- "%-8s" " $ncerts"
			fi
			if ((! showmail)) ; then
				printf -- "%-42s%-40s%s%s\n" "$F" " $eidfield" "$ccol" "$verdict"
			elif (( ${#mbox[@]} == 0 )) ; then
				printf -- "%-80s%-40s%s%s\n" "$F -" " $eidfield" "$ccol" "$verdict"    # no presentable email : keep the column, render it '-'
			else
				for cl in "${mbox[@]}" ; do
					printf -- "%-80s%-40s%s%s\n" "$cl" " $eidfield" "$ccol" "$verdict"
				done
			fi
		}
	done
	((quiet)) && return $rc || return 0
}

bl_pgpid_certify() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage=$"Usage:"" $name [OPTIONS]... [TARGET_U4|TARGET_U5] [TARGET_KEYFPR]"
	local keyserv keyservs=("${BL_PGPID_KEYSERVERS[@]}")

	local helpmsg="
"$"Certify somebody else, identified by its eid TARGET_U4.""
"$"Missing input will be asked interactively."" E.g.: "$"Civil status to calculate TARGET_U4.""

"$"Certification means : I know this other certificate belongs to this real person.""
"$"This implies verifying the civil status and the public key fingerprint of the TARGET.""
"$"This allows you to expand and strengthen your web of trust and those of your close ones.""
"$"This is a commitment: the more you certify, the more you increase your reputation, but if you do it wrong, you will ruin your credibility.""

OPTIONS:
  -u, --use-privkey NAME|KEYID "$"Select private key to use. "$"Default: "$"Guess it from connected token, else ask""
  -E, --all-emails             "$"Also certify every OpenPGP uid containing an email. For compatibility with some legacy software.""
  -R, --revoke                 "$"Revoke your previous certifications on someone else's certificate""
  -o, --ownertrust VALUE       "$"What level of trust do you assign to the target to correctly certify others"" {undefined,marginal,full,never} - "$"Default: ""marginal.
  -l, --local                  "$"« Non-exportable » certification. Pretty useless, except for testing""
  -H, --homedir GNUPGHOME      "$"GnuPG home directory"" - "$"Environnement variable: ""GNUPGHOME, "$"default: ""'~/.gnupg'
  -K, --keyservers KEYSERVERS  "$"If non-empty, receive and send updated certificate from and to this keyservers. "$"Default: ""
$(printf -- "%80s\n" ${keyservs[@]})

"$"Return value:""
-   0 "$"No error""
-   2 "$"Input/Usage error""
- 140 "$"Interactivity error""
- 141 "$"No certificate for user""
- 142 "$"Self-certification is not innovative! ;-)""
- 143 "$"Key fingerprint does not match.""
- "$"Other non-zero on other errors.""
"
	local revoke=0 allemails=0 keyid signcmd="sign" ownertrust
	local gpghome=${GNUPGHOME:-~/.gnupg}
	for ((;$#;)) ; do
		case "$1" in
			-u|--use-privkey)
				shift ; keyid=$1 ;;
			-R|--revoke)
				revoke=1 ;;
			-o|--ownertrust)
				[[ "$2" =~ ^(undefined|marginal|full|never)$ ]] || { printf -- "$FUNCNAME: Error: "$"Unknown ownertrust value '%s'"".\n" "$2" >&2 ; return 2 ;}
				shift ; ownertrust=$1 ;;
			-E|--all-emails|--allemails)
				allemails=1 ;;
			-l|--local)
				signcmd="lsign" ;;
			-k|--keyserver)
				printf -- "$FUNCNAME: Warning: "$"Deprecated option"" '--keyserver' - "$"Please use %s instead.""\n" "'--keyservers'" >&2
				;&
			-K|--keyservers)
				shift ; keyservs=($1) ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local targetid targetfpr keyfpr
	for ((;$#;)) ; do
		if [[ "$1" =~ ${BL_PGPID_UX_REGEX} ]] ; then
			targetid=${BASH_REMATCH[0]}
		elif [[ "${1^^}" =~ ^[ABCDEF0-9]{40}$ ]] ; then
			targetfpr=${BASH_REMATCH[0]}
		else
			printf "$FUNCNAME: Error: "$"Unrecognized argument"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2
			return 2
		fi
		shift
	done

	if [[ -z "$keyid" ]] ; then
		keyid=$(_bl_pgpid_default_key "$gpghome" pgpid_Ckeyfpr $"Private key to use ? (KeyID of your Certification Key)") || return $?
	fi

	# Get fpr for private key to use
	keyfpr=$(gpg --homedir "$gpghome" --list-options show-only-fpr-mbox --list-secret-keys "$keyid" | cut -d ' '  -f 1 | sort -u)

	[[ "$keyfpr" =~ ^[0-9ABCDEF]{40}$ ]] || { printf -- "$FUNCNAME: Error: "$"Invalid or ambiguous private key to use '%s'.""\n" "$keyid" >&2 ; return 2 ;}

	[[ "$targetid" ]] || targetid=$(bl_pgpid_gen_u4)

	# We have to focus on BL_PGPID_U4H_REGEX because BL_PGPID_CO_REGEX part of udid4 is fuzzy (and may be used to distinguish different persons with same BL_PGPID_U4H_REGEX). And we have to include ${BASH_REMATCH[2]::1} because search feature on key server use to search for whole words.
	! [[ "$targetid" =~ (${BL_PGPID_U4H_REGEX})(${BL_PGPID_CO_REGEX}) ]] || targetid="${BASH_REMATCH[1]}${BASH_REMATCH[2]::1}"

	# get fingerprints for certificate to certify
	local fprs ifpr=0 uids
	fprs=($(bl_pgpid_get --fingerprint --homedir "$gpghome" --keyservers "${keyservs[*]}" -- "$targetid")) || return 141
	if (( ${#fprs[@]} >= 2 )) ; then
		printf "$FUNCNAME: Info: "$"User '%s' match many (%d) certificates"".\n" "$targetid" "${#fprs[@]}" >&2
		[[ "$targetfpr" ]] || ifpr=$(bl_radiolist --output-index --text $"Please select key fingerprint" -- "${fprs[@]/#????????????????????????/...}") || return 140
	fi

	if [[ -z "$targetfpr" ]] ; then
		local rep txt=$"Please verify and complete target's fingerprint:"
		local question=".... .... $(sed -E 's,(....),\1 ,g' <<<${fprs[$ifpr]:8:32})"
		for ((;;)) ; do
			rep=$(bl_input --default "$rep" --text "$txt" "$question") || return 140
			rep=$(sed "s,[^ABCDEF0-9],,g" <<<${rep^^} )
			targetfpr="${rep:: 8}${fprs[$ifpr]:8:32}"
			[[ ${#targetfpr} != 40 ]] || break
			txt=$"Enter the first 8 hexadecimal characters of target's key fingerprint:"
		done
	fi

	[[ "$targetfpr" != "$keyfpr" ]] || { echo "$FUNCNAME: Warning: "$"Self-certification is not innovative! ;-)" >&2 ; return 142 ;}
	grep -q "$targetfpr" <<<"${fprs[@]}"|| { echo "$FUNCNAME: Error: "$"Key fingerprint does not match." >&2 ; return 143 ;}

	# Sign only the identity uid (--all-emails opts out) ; legacy certificates
	# fall back to every uid matching $targetid. See bl-pgpid-implementation(7).
	readarray -t uids < <(gpg --homedir "$gpghome" --with-colons --list-key "$targetfpr" | awk -F: '/^pub:/ { if (i>0)  exit } /^uid:[^re]:.*UID.x3aurn.x3aeid.x3a.*'"$targetid"'/ { i+=1 ; print $10 }')
	(( ${#uids[@]} )) || readarray -t uids < <(gpg --homedir "$gpghome" --with-colons --list-key "$targetfpr" | awk -F: '/^pub:/ { if (i>0)  exit } /^uid:[^re]:.*'"$targetid"'/ { i+=1 ; print $10 }')
	# Still no eid u4 ${targetid} is almost impossible since algorthim started with bl_pgpid_get ${targetid}
	(( ${#uids[@]} )) || { printf -- "$FUNCNAME: Crit: "$"No OpenPGP uid containing '%s' on certificate %s. Certificate is probably corrupted.""\n" "$targetid" "$targetfpr"  >&2 ; return 1 ;}
	if ((allemails)) ; then
		local eu u2 dup
		while IFS= read -r eu ; do
			dup=0
			for u2 in "${uids[@]}" ; do
				[[ "$u2" != "$eu" ]] || { dup=1 ; break ; }
			done
			((dup)) || uids+=("$eu")
		done < <(gpg --homedir "$gpghome" --with-colons --list-key "$targetfpr" | awk -F: '/^pub:/ { if (i>0)  exit } /^uid:[^re]:.*<[^<>]+>/ { print $10 }')
	fi
	# Field 10 escapes ':' (and '\\', control bytes) as \\xNN : gpg wants the
	# real uid string back after the '=' exact-match prefix.
	local iu ; for ((iu=0 ; iu<${#uids[@]} ; iu++)) ; do uids[iu]=$(printf '%b' "${uids[iu]}") ; done

	if ((revoke)) ; then
		[[ -z "$ownertrust" ]] || printf -- "$FUNCNAME: Info: "$"%s is non-sense, then ignored, when revoking.""\n" "--ownertrust" >&2
		gpg --homedir "$gpghome" --quick-revoke-sig "$targetfpr" "$keyfpr" "${uids[@]/#/=}" || return $?
		printf "$FUNCNAME: Notice: "$"The sub-layer (%s) return no error, but it may have found nothing to revoke.""\n" "gpg --quick-revoke-sig" >&2
	else
		gpg --homedir "$gpghome" --local-user "$keyfpr" --quick-$signcmd-key "$targetfpr" "${uids[@]/#/=}" || return $?
		printf "$FUNCNAME: Notice: "$"Successfully sign %d uid(s) inside certificate %s.""\n" ${#uids[@]} "$targetfpr" >&2

		# Ownertrust answers "how well does this one certify OTHERS".
		gpg --homedir "$gpghome" --quick-set-ownertrust "$targetfpr" "${ownertrust:-marginal}" || true
	fi

	local ret=0
	for keyserv in ${keyservs[@]} ; do
		gpg --homedir "$gpghome" --keyserver "$keyserv" --send-keys "$targetfpr" || ret=$?
	done
	return $ret
}

bl_pgpid_update_trustdb() {
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [OWNERTRUST.GPG]..."
	local helpmsg="
"$"Recompute the GnuPG trustdb using given ownertrust delegations.""

"$"This is like 'gpg --update-trustdb', but importing first ownertrust defined in
given OWNERTRUST.GPG file(s), IN ORDER.""
"$"Every file MUST be OpenPGP signed message and its signer MUST already be valid,
else the delegation chain is broken.""
"$"That is why OWNERTRUST.GPG order is important.""

OPTIONS:
  -H, --homedir GNUPGHOME  "$"GnuPG home to update (default: ~/.gnupg)""
      --batch              "$"Non-interactive: recompute with 'gpg --check-trustdb', never prompt""
  -q, --quiet              "$"Only Error/Warning output""
"
	local syshome=${GNUPGHOME:-~/.gnupg} quiet=0 batch=0
	for ((;$#;)) ; do
		case "$1" in
			-H|--homedir) shift ; syshome=${1:?} ;;
			--batch)      batch=1 ;;
			-q|--quiet)   quiet=1 ;;
			-h|--help)    printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '%s'\n" "$1" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done
	local -a otfiles=("$@")

	local -a GPG=(gpg --homedir "$syshome" --quiet --batch)

	# Extend the trust with the given signed ownertrust files, if any.
	if (( ${#otfiles[@]} > 0 )) ; then
		# Anchor : the user's own keys are inviolable — a referent may extend trust to
		# others but not redefine the user's trust in themselves, so we strip them out.
		local -a ownfprs ; readarray -t ownfprs < <("${GPG[@]}" --list-secret-keys --with-colons 2>/dev/null | awk -F: '$1=="fpr"{print $10}')

		# Verify every file's signature (needs no trust for that), reject an invalid one,
		# collect its stripped ownertrust, and warn on an out-of-window signature — older
		# than a year, or in the future (a future date usually means a wrong local clock).
		local now oneyearago ; now=$(date +%s) ; oneyearago=$((now - 31536000))
		local otf signer sigepoch content f i l ok
		local -a otcontents=() otsigners=()
		local statusf ; statusf=$(mktemp) || return 1
		trap 'rm -f "$statusf"' RETURN
		for otf in "${otfiles[@]}" ; do
			if ! content=$("${GPG[@]}" --status-fd 3 --decrypt "$otf" 3>"$statusf" 2>/dev/null) ; then
				printf "$FUNCNAME: Error: "$"cannot read or verify %s (missing, not signed, or bad signature?)""\n" "$otf" >&2 ; return 1
			fi
			signer=$(awk '$2=="VALIDSIG"{print $3; exit}' "$statusf")
			sigepoch=$(awk '$2=="VALIDSIG"{print $5; exit}' "$statusf")
			[[ "$signer" ]] || { printf "$FUNCNAME: Error: "$"%s carries no verifiable signature (signer key missing?)""\n" "$otf" >&2 ; return 1 ; }
			if [[ "$sigepoch" =~ ^[0-9]+$ ]] && (( sigepoch > now )) ; then
				printf "$FUNCNAME: Warning: "$"%s is signed in the future (%s) — is the local clock right?""\n" "$otf" "$(date --utc --date=@"$sigepoch" +%Y-%m-%d)" >&2
			elif [[ "$sigepoch" =~ ^[0-9]+$ ]] && (( sigepoch < oneyearago )) ; then
				printf "$FUNCNAME: Warning: "$"%s carries a delegation older than a year (%s)""\n" "$otf" "$(date --utc --date=@"$sigepoch" +%Y-%m-%d)" >&2
			fi
			# The raw content must actually be ownertrust ('FINGERPRINT:TRUST:' lines), else
			# the file is not a delegation and we refuse it rather than silently ignore it.
			# Validate BEFORE stripping the anchor, so a file that only trusts the anchor
			# (legitimately stripped to nothing) is not mistaken for garbage.
			ok=0
			while IFS= read -r l ; do
				[[ -n "$l" && "$l" != \#* ]] || continue
				[[ "$l" =~ ^[0-9A-Fa-f]{16,}:[0-9]{1,3}: ]] || { ok=0 ; break ; }
				ok=1
			done <<<"$content"
			((ok)) || { printf "$FUNCNAME: Error: "$"%s is not a valid ownertrust file""\n" "$otf" >&2 ; return 1 ; }
			for f in "${ownfprs[@]}" ; do content=$(grep -v "^$f:" <<<"$content" || true) ; done
			otcontents+=("$content") ; otsigners+=("$signer")
		done

		# Idempotence : compute the prospective ownertrust textually (later files override
		# earlier ones, per fingerprint). Apply only if it differs from the current one.
		local before after
		before=$("${GPG[@]}" --export-ownertrust 2>/dev/null | grep -v '^#' | sort)
		after=$( { printf '%s\n' "$before" ; printf '%s\n' "${otcontents[@]}" | grep -v '^#' ; } \
			| awk -F: 'NF>=2 && $1 ~ /^[0-9A-Fa-f]+$/ {t[$1]=$2} END{for(k in t) print k":"t[k]":"}' | sort )
		if [[ "$after" == "$before" ]] ; then
			((quiet)) || printf "$FUNCNAME: Info: "$"the delegations are already applied — ownertrust left unchanged""\n" >&2
		else
			# Back up the current ownertrust before mutating, in a dedicated timestamped file.
			local backupdir="$syshome/ownertrust-backups" backupf
			mkdir -p "$backupdir" || return 1
			backupf="$backupdir/ownertrust-$(date --utc +%Y%m%dT%H%M%SZ).txt"
			"${GPG[@]}" --export-ownertrust > "$backupf" 2>/dev/null
			printf "$FUNCNAME: Notice: "$"current ownertrust backed up in %s""\n" "$backupf" >&2

			# Apply the ordered delegation chain : each file's signer must already be valid
			# (GnuPG auto-recomputes on access, so file N+1 sees file N's effect).
			for i in "${!otfiles[@]}" ; do
				signer=${otsigners[i]}
				[[ "$("${GPG[@]}" --with-colons --list-keys "0x$signer" 2>/dev/null | awk -F: '$1=="uid" && ($2=="f"||$2=="u"){print 1; exit}')" ]] \
					|| { printf "$FUNCNAME: Error: "$"delegation chain broken: the signer of %s is not (yet) valid""\n" "${otfiles[i]}" >&2 ; return 1 ; }
				printf '%s\n' "${otcontents[i]}" | "${GPG[@]}" --import-ownertrust 2>/dev/null
			done

			# Report the change as a human-readable unified diff of the sorted ownertrust,
			# and how to undo it.
			((quiet)) || {
				after=$("${GPG[@]}" --export-ownertrust 2>/dev/null | grep -v '^#' | sort)
				printf "$FUNCNAME: Info: "$"ownertrust changes:""\n" >&2
				diff --unified --color=auto --label old-ownertrust --label new-ownertrust \
					<(printf '%s\n' "$before") <(printf '%s\n' "$after") >&2 || true
			}
			printf "$FUNCNAME: Notice: "$"to restore the previous trust: ""gpg --homedir %s --import-ownertrust %s && gpg --homedir %s --batch --check-trustdb\n" "$syshome" "$backupf" "$syshome" >&2
		fi
	fi

	# Recompute the trustdb : interactively complete the ownertrust of the remaining keys
	# ('gpg --update-trustdb'), unless --batch selects the non-interactive 'gpg --check-trustdb'.
	if ((batch)) ; then
		gpg --homedir "$syshome" --batch --check-trustdb 2>/dev/null
	else
		gpg --homedir "$syshome" --update-trustdb
	fi
}

bl_pgpid_print() (
	local name
	((BL_PGPID_isprogram)) && name="$BL_PGPID_NAME ${FUNCNAME:9}" || name="$FUNCNAME"
	local usage="Usage: $name [OPTIONS]... [NAME|EMAIL|KEYID|U4|U5]"
	local helpmsg="
"$"Produce or print an OpenPGP ID stamp or business card.""
"$"Missing NAME|EMAIL|KEYID|U4|U5 => Guess it using security token, or interactively ask KEYID of secret key.""
"$"A single email appears on the output: if the argument is an email it is used verbatim; otherwise the most recent non-revoked email of the certificate is picked.""

OPTIONS:
  -P, --print PRINTER|FILE.svg  "$"Printer name to send to, or output SVG file if it ends with '.svg'""
  -t, --template FILE.svg       "$"Use this template to produce business card (eg: …/share/bash-libs/svg/card.svg)""
  -N, --name NAME               "$"Override the displayed name"$" (default: guessed from OpenPGP certificate and email)""
  -g, --no-color                "$"Output in grayscale instead of color""
  -H, --homedir GNUPGHOME       "$"GnuPG home directory (default: ~/.gnupg). "$"Environment variable: ""\$GNUPGHOME
"

	local nocolor=0 printer usename usetemplate
	local gpghome=${GNUPGHOME:-~/.gnupg}
	for ((;$#;)) ; do
		case "$1" in
			-P|--print|--printer)
				shift ; printer=${1:?} ;;
			-t|--template)
				shift ; usetemplate=${1:?} ;;
			-g|--no-color|--gray|--grayscale)
				nocolor=1 ;;
			-N|--usename|--name)
				shift ; usename=${1:-} ;;
			-H|--homedir)
				shift ; gpghome=${1:?} ;;
			-h|--help) printf "%s\n%s\n" "$usage" "$helpmsg" ; return ;;
			-V|--version) printf "%s %s\n" "$FUNCNAME" "$BL_PGPID_VERSION" ; return ;;
			--) shift ; break ;;
			-*) printf "$FUNCNAME: Error: "$"Unrecognized option"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$name" >&2 ; return 2 ;;
			*) break ;;
		esac
		shift
	done

	local tmpdir
	tmpdir=$(mktemp --directory -t "$FUNCNAME".XXXXXX) || return $?
	trap "rm -rf '$tmpdir'" EXIT

	# Resolve user → fingerprint, and email
	local user fpr email
	if [[ "$1" ]] ; then
		user=$1
	else
		user=$(_bl_pgpid_default_key "$gpghome" pgpid_Skeyfpr $"Select certificate to print") || return $?
	fi

	if [[ "$user" =~ ^${BL_INTERACTIVE_EMAIL_REGEX}$ ]] ; then
		email=$user
		# Identify the certificate by email, but only count those with a non-revoked uid for this email
		local -a allfprs candidates=() f
		mapfile -t allfprs < <(bl_pgpid_get --fingerprint --no-fetch --homedir "$gpghome" -- "$email" 2>/dev/null) || true
		for f in "${allfprs[@]}" ; do
			if gpg --homedir "$gpghome" --with-colons --list-keys "$f" 2>/dev/null |
			   awk -F: -v EM="<$email>" '
				/^pub:[^r]:/ { cert_ok=1; next }
				/^pub:r/      { cert_ok=0; next }
				/^uid:[^r]:/  { if (cert_ok && index($10, EM)) ok=1 }
				END { exit !ok }
			   ' ; then
				candidates+=("$f")
			fi
		done
		(( ${#candidates[@]} > 0 )) || { printf "$FUNCNAME: Error: "$"No usable certificate for %s.""\n" "$email" >&2 ; return 1 ; }
		(( ${#candidates[@]} == 1 )) || { printf "$FUNCNAME: Error: "$"Email %s matches several non-revoked certificates (%d).""\n" "$email" "${#candidates[@]}" >&2 ; return 1 ; }
		fpr=${candidates[0]}
	else
		local -a fprs
		mapfile -t fprs < <(bl_pgpid_get --fingerprint --errexit-g=1 --no-fetch --homedir "$gpghome" -- "$user") || return $?
		fpr=${fprs[0]}
	fi

	# Most-recent non-revoked email if not already set
	if [[ -z "$email" ]] ; then
		email=$(gpg --homedir "$gpghome" --with-colons --list-keys "$fpr" 2>/dev/null |
			awk -F: '/^uid:[^r]:/ {
				em = $10
				if (em ~ /<[^>]+@[^>]+>/) {
					sub(/.*</, "", em); sub(/>.*/, "", em)
					print $6, em
				}
			}' | sort -rn -k1 | awk 'NR==1 {print $2}')
		[[ "$email" ]] || { printf "$FUNCNAME: Error: "$"No usable (non-revoked) email in certificate %s.""\n" "$fpr" >&2 ; return 1 ; }
		printf "$FUNCNAME: Notice: "$"Picking most recent email %s.""\n" "$email" >&2
	fi

	# Pretty name: --name overrides ; else a FN: vCard-property uid (the vCard-uid
	# process) ; else the name part of an old name-addr uid attached to the email ;
	# else the email local-part. --with-colons escapes ':' as \\xNN in field 10,
	# so each candidate is decoded before matching.
	local pname=$usename
	local _u
	if [[ -z "$pname" ]] ; then
		while read -r _u ; do
			_u=$(printf '%b' "$_u")
			[[ "$_u" == FN:* ]] || continue
			pname=${_u#FN:} ; break
		done < <(gpg --homedir "$gpghome" --with-colons --list-keys "$fpr" 2>/dev/null | awk -F: '/^uid:[^r]:/{print $10}')
	fi
	if [[ -z "$pname" ]] ; then
		pname=$(gpg --homedir "$gpghome" --with-colons --list-keys "$fpr" 2>/dev/null |
			awk -F: -v EM="<$email>" '/^uid:[^r]:/ {
				if (index($10, EM)) {
					n = $10
					sub(/ *\([^)]*\) *<[^>]+>$/, "", n); sub(/ *<[^>]+>$/, "", n)
					print n ; exit
				}
			}')
		pname=$(printf '%b' "$pname")
		# A vCard property prefix (from an email-only uid) is not a display name.
		[[ ! "$pname" =~ ^(EMAIL|UID|NOTE|FN): ]] || pname=""
	fi
	[[ "$pname" ]] || pname=${email%%@*}

	# eid (u4 or u5) in its standard glued form, if present in the certificate
	local pgpid
	pgpid=$(gpg --homedir "$gpghome" --with-colons --list-keys "$fpr" 2>/dev/null | _bl_pgpid_eids_of | head -n1)

	# Format fingerprint as 2 lines of 5 groups of 4 hex
	local fprspaced fprline1 fprline2
	fprspaced=$(sed -E 's,([0-9A-F]{4}),\1 ,g; s/ *$//' <<<"${fpr^^}")
	fprline1=$(awk '{ for(i=1;i<=5;i++) printf "%s%s", $i, (i==5?"":" "); print "" }' <<<"$fprspaced")
	fprline2=$(awk '{ for(i=6;i<=10;i++) printf "%s%s", $i, (i==10?"":" "); print "" }' <<<"$fprspaced")

	# Resolve template:
	#   no --template  → default inline sticker (single line in tmpdir)
	#   --template FILE → use FILE if readable, else warn and fall back to card.svg
	local template
	if [[ "$usetemplate" ]] ; then
		if [[ -r "$usetemplate" ]] ; then
			template=$usetemplate
		else
			template="$(dirname "$(readlink -f "$BASH_SOURCE")")/../share/bash-libs/svg/card.svg"
			printf "$FUNCNAME: Warning: "$"Card template %s is not readable, falling back to default %s.""\n" "${usetemplate@Q}" "${template@Q}" >&2
			[[ -r "$template" ]] || { printf "$FUNCNAME: Error: "$"Default card.svg template not found.""\n" >&2 ; return 1 ;}
		fi
	else
		template="$tmpdir/sticker.svg"
		printf '%s' "$BL_PGPID_STICKER_TEMPLATE" > "$template" || return $?
	fi

	# QR code (sticker AND card — card embeds the sticker block verbatim).
	# Links to keyserver index of this fingerprint.
	# qrencode --margin=3: ~0.5 mm thicker visible quiet zone vs --margin=2.
	local qrhref=""
	local qrurl="https://keys.foopgp.org/pks/lookup?op=index&fingerprint=on&search=0x${fpr^^}"
	qrencode --output "$tmpdir/qr.png" -- "$qrurl" || return $?
	qrhref="data:image/png;base64,$(base64 -w0 < "$tmpdir/qr.png")"

	# Render template via envsubst (only substitute the names we own — avoid clobbering CSS ${...} should any appear)
	local subtitle=$"Friends of OpenPGP"
	NAME=$pname SUBTITLE=$subtitle U4=$pgpid \
		FPR_LINE1=$fprline1 FPR_LINE2=$fprline2 \
		EMAIL=$email QR_HREF=$qrhref \
		envsubst '$NAME $SUBTITLE $U4 $FPR_LINE1 $FPR_LINE2 $EMAIL $QR_HREF' \
		< "$template" > "$tmpdir/render.svg"

	# Grayscale post-process by remapping the small palette used in the templates
	if ((nocolor)) ; then
		sed -i -E '
			s,#ff8012,#666666,g
			s,#90462f,#888888,g
			s,#ffefea,#f0f0f0,g
			s,#2c284b,#222222,g
			s,#5d4ce6,#555555,g
			s,#fafafa,#ffffff,g
		' "$tmpdir/render.svg"
	fi

	# Output to file if asked
	if [[ "$printer" == *.svg ]] ; then
		cp -- "$tmpdir/render.svg" "$printer" || return $?
		printf "$FUNCNAME: Notice: "$"Wrote SVG to %s.""\n" "$printer" >&2
		return 0
	fi

	# Else: render to PDF, multi-up on A4, send to printer
	if [[ -z "$printer" ]] ; then
		local -a printers
		printers=($(LANG= lpstat -p | sed -n 's,^printer \([^ ]*\).*,\1,p'))
		[[ "${printers[0]}" ]] || { printf "%s: Error: "$"No printer detected.""\n" "$FUNCNAME" >&2 ; return 1 ; }
		printer=$(bl_radiolist --output-value --default-value "$(lpstat -d | sed 's,.* ,,')" --num-per-line 1 --text $"Where to print ?" "${printers[@]}")
	fi

	rsvg-convert --format=pdf --output="$tmpdir/single.pdf" "$tmpdir/render.svg" || return $?

	# A4 landscape: sticker 85×25mm → 3×7 = 21/page ; --template card → 3×3 = 9/page.
	# pdfjam tiles ONE page per --nup slot, so duplicate single.pdf N times first.
	# --noautoscale true keeps the natural card/sticker size : without it the
	# output is ~18% oversized and bleeds past the printable area.
	local nup n i
	[[ "$usetemplate" ]] && { nup="3x3" ; n=9 ; } || { nup="3x7" ; n=21 ; }
	local -a copies=()
	for ((i=0 ; i<n ; i++)) ; do copies+=("$tmpdir/single.pdf") ; done
	pdfunite "${copies[@]}" "$tmpdir/duped.pdf" || return $?
	pdfjam --quiet --paper a4paper --landscape --nup "$nup" --noautoscale true --outfile "$tmpdir/page.pdf" "$tmpdir/duped.pdf" || return $?

	printf "$FUNCNAME: Notice: "$"Printing %s %s on %s…""\n" "$nup" "$(basename -- "$template")" "$printer" >&2
	lpr -P "$printer" "$tmpdir/page.pdf"
)

### Init ###

if ((BL_PGPID_isprogram)) ; then
	BL_PGPID_usage="Usage: $BASH_SOURCE [MAIN_OPTIONS]... ACTION [OPTIONS]... [ARGUMENTS]..."
	BL_PGPID_shelpmsg="
  -h, --help              "$"Show help and exit.""
  -V, --version           "$"Show version and exit.""

ACTIONS:
$(for f in "${BL_PGPID_FUNCTIONS[@]}" ; do printf "   %-19s %s\n" "${f:9}" "$($f --help | sed -n '/^$/{n;p;q}')" ; done)

All actions support a --help option, eg:
$ $BASH_SOURCE ${BL_PGPID_FUNCTIONS:9} --help

$BL_PGPID_NAME is also bash library, see:
$ source $BASH_SOURCE --help
"
else
	BL_PGPID_usage="Usage: source $BASH_SOURCE [MAIN_OPTIONS]..."
	BL_PGPID_shelpmsg="
      --bash-completion    set completion for $BASH_SOURCE program and return (without loading anything else)

Functions:
$(for f in "${BL_PGPID_FUNCTIONS[@]}" ; do printf "   %-19s %s\n" "$f" "$($f --help | sed -n '/^$/{n;p;q}')" ; done)

Reminder: when used as a library, all functions calls share the same environment variables, i.e. the same global options.
"
fi

# Parse Options
_BL_PGPID_RETVAL=0
_bl_pgpid_parseoptions "$@" || _BL_PGPID_RETVAL=$?

# Do nothing else if sourced
[[ "$BASH_SOURCE" == "$0" ]] || return $_BL_PGPID_RETVAL

[[ "$_BL_PGPID_RETVAL" != "1"  ]] || exit 0
[[ "$_BL_PGPID_RETVAL" -lt 2 ]] || exit $_BL_PGPID_RETVAL

### Run ###
set -e
shift $BL_PGPID_NOPTIONS
if function=$(grep -o "\<bl_pgpid_$1\>" <<< "${BL_PGPID_FUNCTIONS[@]}") ; then
	shift
	$function "$@"
	exit $?
else
	printf "$BL_PGPID_NAME: Error: "$"Unrecognized action"" '$1'.\n\n"$"Try '%s --help' for more information"".\n" "$BASH_SOURCE" >&2
	exit 2
fi

