From: Arnout Vandecappelle <arnout@mind.be>
To: buildroot@busybox.net
Subject: [Buildroot] [PATCH v6 07/13] support/scripts: add fix-rpath script + a bunch of helpers
Date: Mon, 1 Feb 2016 22:50:25 +0100 [thread overview]
Message-ID: <56AFD321.5000006@mind.be> (raw)
In-Reply-To: <1454342021-22960-8-git-send-email-s.martin49@gmail.com>
On 01-02-16 16:53, Samuel Martin wrote:
> This commit introduces a fix-rpath shell-script able to scan a tree,
> detect ELF files, check their RPATH and fix it in a proper way.
>
> Along to the fix-rpath script, it also adds a bunch of shell helper
> functions grouped into modules. This will help writing scripts handling
> RPATH and other things, while allowing to share and reuse these
> functions between scripts.
>
> These helpers are namespaced within the filename of the module in which
> they are gathered.
>
> This change adds 6 modules:
> - source.sh: provides simple helper to easily source another module, taking
> care of not sourcing again an already-sourced one;
> - log.sh: provides logging helpers;
> - utils.sh: provides simple functions to filter ELF files in a list;
> - readelf.sh: provides functions retrieving ELF details from a file;
> - patchelf.sh: provides function updating ELF files;
> - sdk.sh: provides RPATH computation functions.
>
> These 6 modules are used by the fix-rpath script.
> Follow-up patches will make some scripts leveraging these modules.
>
> Signed-off-by: Samuel Martin <s.martin49@gmail.com>
>
> ---
> changes v5->v6:
> - fully rewritten in shell
>
> changes v4->v5:
> - add verbose support
> - rename shrink_rpath -> clear_rpath
> - add sanitize_rpath function
>
> changes v3->v4:
> - fix typos and license (Baruch)
>
> changes v2->v3:
> - no change
> ---
> support/scripts/fix-rpath | 101 +++++++++++++++++++++++
> support/scripts/shell/log.sh | 57 +++++++++++++
> support/scripts/shell/patchelf.sh | 163 ++++++++++++++++++++++++++++++++++++++
> support/scripts/shell/readelf.sh | 52 ++++++++++++
> support/scripts/shell/sdk.sh | 70 ++++++++++++++++
> support/scripts/shell/source.sh | 73 +++++++++++++++++
> support/scripts/shell/utils.sh | 60 ++++++++++++++
> 7 files changed, 576 insertions(+)
> create mode 100755 support/scripts/fix-rpath
> create mode 100644 support/scripts/shell/log.sh
> create mode 100644 support/scripts/shell/patchelf.sh
> create mode 100644 support/scripts/shell/readelf.sh
> create mode 100644 support/scripts/shell/sdk.sh
> create mode 100644 support/scripts/shell/source.sh
> create mode 100644 support/scripts/shell/utils.sh
>
> diff --git a/support/scripts/fix-rpath b/support/scripts/fix-rpath
> new file mode 100755
> index 0000000..938e599
> --- /dev/null
> +++ b/support/scripts/fix-rpath
> @@ -0,0 +1,101 @@
> +#!/usr/bin/env bash
> +
> +# Copyright (C) 2016 Samuel Martin <s.martin49@gmail.com>
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 2 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> +# General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with this program; if not, write to the Free Software
> +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
> +
> +usage() {
> + cat <<EOF >&2
> +Usage: ${0} TREE_KIND TREE_ROOT
> +
> +Description:
> +
> + This script scans a tree and sanitize ELF files' RPATH found in there.
... sanitizes the RPATH of ELF files found in there.
> +
> + Sanitization behaves the same whatever the kindd of the processed tree, but
Please wrap at 80 columns. But actually, I think 4 spaces is enough for
indentation, that's what you use below as well.
kindd -> kind
But actually I don't really understand this sentence (well, I do after reading
the rest). So I'd just remove this sentence and add...
> + the resulting RPATH differs.
> +
> + Sanitization action:
Sanitization action (for all trees):
> + - remove RPATH pointing outside of the tree
> + - for RPATH pointing in the tree:
> + - if they point to standard location (/lib, /usr/lib): remove them
> + - otherwise: make them relative using \$ORIGIN
> +
> + For the target tree:
> + - scan the whole tree for sanitization
> +
> + For the staging tree :
> + - scan the whole tree for sanitization
> +
> + For the host tree:
> + - skip the staging tree for sanitization
> + - add \$HOST_DIR/{lib,usr/lib} to RPATH (as relative pathes)
pathes -> paths
> +
> +Arguments:
> +
> + TREE_KIND Kind of tree to be processed.
> + Allowed values: host, target, staging
> +
> + TREE_ROOT Path to the root of the tree to be scaned
scanned
> +
> +EOF
> +}
> +
> +source "${0%/*}/shell/source.sh"
> +
> +source.load_module utils
> +source.load_module readelf
> +source.load_module patchelf
What is this, python? :-) I think this source.sh is a bit over the top (it
doesn't really hurt to source those modules a second time), but why not.
> +
> +main() {
> + local tree="${1}"
> + local basedir="$( readlink -f "${2}" )"
> +
> + local find_args=( "${basedir}" )
> + local sanitize_extra_args=()
> + local readelf
> +
> + case "${tree}" in
> + host)
> + # do not process the sysroot (only contains target binaries)
> + find_args+=( "-name" "sysroot" "-prune" "-o" )
There may be some other random file that is called sysroot, so to be safe I'd
make this
find_args+=( "-path" "${basedir}/usr/*/sysroot" "-prune" "-o" )
(untested).
> +
> + # do not process the external toolchain installation directory to
> + # to avoid breaking it.
> + find_args+=( "-path" "*/opt/ext-toolchain" "-prune" "-o" )
Again, to be safe against a opt/ext-toolchain appearing somewhere else in the
tree (admittedly, this is ridiculously unlikely), use . instead of *.
> +
> + # make sure RPATH will point to ${hostdir}/lib and ${hostdir}/usr/lib
> + sanitize_extra_args+=( "keep_lib_and_usr_lib" )
Since sanitize_extra_args only contains a single argument, I don't think it's
worth making it an array. Just assign empty above and assign
keep_lib_and_usr_lib here, and pass unquoted below. But that's just MHO.
> +
> + readelf="${HOST_READELF}"
> + ;;
> + staging|target)
> + readelf="${TARGET_READELF}"
> + ;;
> + *)
> + usage
> + exit 1
> + ;;
> + esac
> +
> + find_args+=( "-type" "f" "-print" )
> +
> + while read file; do
> + READELF="${READELF}" PATCHELF="${PATCHELF}" \
Shouldn't that be ${readelf}? And where does ${PATCHELF} come from? If it comes
from the environment, there's no need to export it again here.
But maybe it's cleaner to just export READELF inside the case statement, above.
> + patchelf.sanitize_rpath "${basedir}" "${file}" ${sanitize_extra_args[@]}
> + done < <( find ${find_args[@]} | utils.filter_elf )
I personally don't like this reverse construct, and would write:
find ${find_args[@]} | utils.filter_elf | \
while read file; do
...
Again, MHO.
> +}
> +
> +main ${@}
> diff --git a/support/scripts/shell/log.sh b/support/scripts/shell/log.sh
> new file mode 100644
> index 0000000..ffa19e8
> --- /dev/null
> +++ b/support/scripts/shell/log.sh
> @@ -0,0 +1,57 @@
> +# Copyright (C) 2016 Samuel Martin <s.martin49@gmail.com>
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 2 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> +# General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with this program; if not, write to the Free Software
> +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
> +
> +# Logging helpers
> +#
> +# This module defines the following functions:
> +# log.trace
> +# log.debug
> +# log.info
> +# log.warn
> +# log.errorN
> +# log.error
> +#
> +# This module sets the following variables:
> +# my_name
> +#
> +# This module is sensitive to the following environment variables:
> +# DEBUG
> +
> +source.declare_module log
> +
> +# Debug level:
> +# - 0 or empty: only show errors
> +# - 1 : show errors and warnings
> +# - 2 : show errors, warnings, and info
> +# - 3 : show errors, warnings, info and debug
> +: ${DEBUG:=0}
> +
> +# Low level utility function
> +log.trace() { local msg="${1}"; shift; printf "%s: ${msg}" "${my_name}" "${@}"; }
I'd do the redirect directly here in the printf, rather than below.
Also, I see no reason to put it all on one line.
> +
> +# Public logging functions
> +log.debug() { :; }
> +[ ${DEBUG} -lt 3 ] || log.debug() { log.trace "${@}" >&2; }
> +log.info() { :; }
> +[ ${DEBUG} -lt 2 ] || log.info() { log.trace "${@}" >&2; }
> +log.warn() { :; }
> +[ ${DEBUG} -lt 1 ] || log.warn() { log.trace "${@}" >&2; }
> +log.errorN() { local ret="${1}"; shift; log.warn "${@}"; exit ${ret}; }
> +log.error() { log.errorN 1 "${@}"; }
> +
> +# Program name
> +my_name="${0##*/}"
Isn't my_name always going to be log.sh? "source" doesn't propagate the
parent's arguments AFAIK.
> +
> diff --git a/support/scripts/shell/patchelf.sh b/support/scripts/shell/patchelf.sh
> new file mode 100644
> index 0000000..d1eb590
> --- /dev/null
> +++ b/support/scripts/shell/patchelf.sh
> @@ -0,0 +1,163 @@
> +# Copyright (C) 2016 Samuel Martin <s.martin49@gmail.com>
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 2 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> +# General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with this program; if not, write to the Free Software
> +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
> +
> +# Patchelf helpers
AFAICS these helper will only ever be used in the fix-rpaths script (but I
haven't read the entire series yet). Unless you can see a good use case for
using these functions in another script, I would prefer to see them defined
directly in the fix-rpaths script rather than a separate module. IMHO it's
easier to understand related code if it's in one file than if it's in separate
files.
> +#
> +# This module defines the following functions:
> +# patchelf.set_xrpath
> +# patchelf.update_rpath
> +# patchelf.sanitize_rpath
> +#
> +# This module is sensitive to the following environment variables:
> +# PATCHELF
> +# READELF
> +
> +source.declare_module patchelf
> +
> +source.load_module log
> +source.load_module sdk
> +source.load_module utils
> +source.load_module readelf
> +
> +: ${PATCHELF:=patchelf}
> +
> +# patchelf.set_xrpath file rpath...
> +#
> +# Set RPATH in $file.
> +# Automatically join all RPATH with the correct separator, and also handle
> +# XRPATH (replacing "XORIGIN" with "$ORIGIN").
> +#
> +# file : ELF file path
> +# rpath : RPATH element
> +#
> +# environment:
> +# PATCHELF: patchelf program path
> +patchelf.set_xrpath() {
> + local file="${1}"
> + shift
> + local xrpath="$(sed -e 's/ +/:/g' <<<"${@}")"
In fix-rpath you put spaces inside the "$( ... )". I think this would be a good
idea here as well.
> + "${PATCHELF}" --set-rpath "${xrpath//XORIGIN/\$ORIGIN}" "${file}"
> +}
> +
> +# patchelf.update_rpath basedir binary libdirs...
> +#
> +# Set RPATH in $binary computing them from the paths $libdirs (and $basedir).
> +# Existing RPATH in $file will be overwritten if any.
$file -> $binary ?
> +#
> +# basedir : absolute path of the tree in which the $bindir and $libdirs must be
> +# binary : ELF file absolute path
> +# libdirs : list of library location (absolute paths)
location -> locations
> +#
> +# environment:
> +# PATCHELF: patchelf program path
> +patchelf.update_rpath() {
> + local basedir="${1}"
> + local binary="${2}"
> + shift 2
> + local libdirs=( ${@} )
> + log.debug " basedir: %s\n" "${basedir}"
> + log.debug " elf: %s\n" "${binary}"
> + log.debug " libdirs: %s\n" "${libdirs[*]}"
> + log.info " rpath: %s\n" \
> + "$( sdk.compute_xrpath "${basedir}" "${binary%/*}" ${libdirs[@]} |
> + sed -e 's/XORIGIN/\$ORIGIN/g' )"
Maybe compute it once and store it in a variable?
Alternatively, add a helper
log.tracerun() {
log.info "%s\n" "$*"
"$@"
}
and use it below.
> + PATCHELF="${PATCHELF}" patchelf.set_xrpath "${binary}" \
Again, PATCHELF is already in the environment.
> + $( sdk.compute_xrpath "${basedir}" "${binary%/*}" ${libdirs[@]} )
> +}
> +
> +# patchelf.sanitize_rpath basedir binary [keep_lib_usr_lib]
> +#
> +# Scan $binary's RPATH, remove any of them pointing outside of $basedir.
> +# If $keep_lib_usr_lib in not empty, the library directories $basedir/lib and
in -> is
> +# $basedir/usr/lib will be added to the RPATH.
> +#
> +# Note:
> +# Absolute paths is needed to correctly handle symlinks and or mount-bind in
is -> are
and or -> and/or
mount-bind -> bind-mount (though actually, I don't see how you're going to get
an absolute path through a bind mount)
> +# the $basedir path.
> +#
> +# basedir : absolute path of the tree in which the $bindir and $libdirs
> +# must be
> +# binary : ELF file absolute path
> +# keep_lib_usr_lib : add to RPATH $basedir/lib and $basedir/usr/lib
> +#
> +# environment:
> +# PATCHELF: patchelf program path
> +# READELF : readelf program path
> +patchelf.sanitize_rpath() {
> + local basedir="$( readlink -f "${1}" )"
> + local binary="${2}"
> + local keep_lib_usr_lib="${3}"
> +
> + local path abspath rpath
> + local libdirs=()
I think new_rpaths is a better name here.
> +
> + if test -n "${keep_lib_usr_lib}" ; then
> + libdirs+=( "${basedir}/lib" "${basedir}/usr/lib" )
> + fi
> +
> + log.info "ELF: %s\n" "${binary}"
> +
> + local rpaths="$( READELF="${READELF}" readelf.get_rpath "${binary}" )"
Again, READELF is already in the environment.
> +
> + for rpath in ${rpaths//:/ } ; do
> + # figure out if we should keep or discard the path; there are several
> + # cases to handled:
> + # - $path starts with "$ORIGIN":
> + # The original build-system already took care of setting a relative
> + # RPATH, resolve it and test if it is worthwhile to keep it;
> + # - $basedir/$path exists:
> + # The original build-system already took care of setting an absolute
> + # RPATH (absolute in the final rootfs), resolve it and test if it is
> + # worthwhile to keep it;
> + # - $path start with $basedir:
start -> starts
> + # The original build-system added some absolute RPATH (absolute on
> + # the build machine). While this is wrong, it can still be fixed; so
> + # test if it is worthwhile to keep it;
> + # - $path points somewhere else:
> + # (can be anywhere: build trees, staging tree, host location,
> + # non-existing location, etc.)
> + # Just discard such a path.
This is not correct for host packages. If a host package has picked up an RPATH
to e.g. ~/lib/foo through pkg-config, then that RPATH must still be kept. But
only for host packages. For host packages, you should probably just not remove
anything.
So I think you still need to distinguish between the host and staging/target
cases here. In fact, I think the whole logic of this function is easier to
understand if it is separate for host and target/staging.
> + if grep -q '^$ORIGIN/' <<<"${rpath}" ; then
> + path="${binary%/*}/${rpath#*ORIGIN/}"
Maybe ?ORIGIN instead of *ORIGIN, just in case there's another ORIGIN somewhere
in the path?
> + elif test -e "${basedir}/${rpath}" ; then
-e -> -d, it really must be a directory.
> + path="${basedir}/${rpath}"
> + elif grep -q "^${basedir}/" <<<"$( readlink -f "${rpath}" )" ; then
> + path="${rpath}"
> + else
> + log.debug "\tDROPPED [out-of-tree]: %s\n" "${rpath}"
> + continue
> + fi
> +
> + abspath="$( readlink -f "${path}" )"
> +
> + # discard path pointing to default locations handled by ld-linux
ld-linux -> ld.so
> + if grep -qE "^${basedir}/(lib|usr/lib)$" <<<"${abspath}" ; then
> + log.debug \
> + "\tDROPPED [std libdirs]: %s (%s)\n" "${rpath}" "${abspath}"
> + continue
> + fi
> +
> + log.debug "\tKEPT %s (%s)\n" "${rpath}" "${abspath}"
> +
> + libdirs+=( "${abspath}" )
> +
> + done
> +
> + libdirs=( $( utils.list_reduce ${libdirs[@]} ) )
> +
> + PATCHELF="${PATCHELF}" \
Again, already in the env.
> + patchelf.update_rpath "${basedir}" "${binary}" ${libdirs[@]}
> +}
> diff --git a/support/scripts/shell/readelf.sh b/support/scripts/shell/readelf.sh
> new file mode 100644
> index 0000000..82968a2
> --- /dev/null
> +++ b/support/scripts/shell/readelf.sh
> @@ -0,0 +1,52 @@
> +# Copyright (C) 2016 Samuel Martin <s.martin49@gmail.com>
> +#
> +# This program is free software; you can redistribute it and/or modify
> +# it under the terms of the GNU General Public License as published by
> +# the Free Software Foundation; either version 2 of the License, or
> +# (at your option) any later version.
> +#
> +# This program is distributed in the hope that it will be useful,
> +# but WITHOUT ANY WARRANTY; without even the implied warranty of
> +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
> +# General Public License for more details.
> +#
> +# You should have received a copy of the GNU General Public License
> +# along with this program; if not, write to the Free Software
> +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
> +
> +# Readelf helpers
> +#
> +# This module defines the following functions:
> +# readelf.get_rpath
> +#
> +# This module is sensitive to the following environment variables:
> +# READELF
> +#
> +# This module sets the following environment variables:
> +# LC_ALL=C
> +
> +source.declare_module readelf
> +
> +# Override the user's locale so we are sure we can parse the output of
> +# readelf(1) and file(1)
> +export LC_ALL=C
> +
> +: ${READELF:=readelf}
> +
> +# readelf.get_rpath file
> +#
> +# Return the unsplitted RPATH/RUNPATH of $file.
unsplitted -> unsplit.
But it's actually not return, it's print. And to make it very explicit, write:
Print the :-separate RPATH/RUNPATH of $file.
> +#
> +# To split the returned RPATH string and store them in an array, do:
> +#
> +# paths=( $(readelf.get_rpath "${file}" | sed -e 's/:/ /g') )
> +#
> +# file : ELF file path
> +#
> +# environment:
> +# READELF: readelf program path
> +readelf.get_rpath() {
> + local file="${1}"
> + "${READELF}" --dynamic "${file}" |
> + sed -r -e '/.* \(R(UN)?PATH\) +Library r(un)?path: \[(.+)\]$/!d ; s//\3/'
> +}
I have to go now, so reviewed until here. The rest I can hopefully review
tomorrow...
Regards,
Arnout
[snip]
--
Arnout Vandecappelle arnout at mind be
Senior Embedded Software Architect +32-16-286500
Essensium/Mind http://www.mind.be
G.Geenslaan 9, 3001 Leuven, Belgium BE 872 984 063 RPR Leuven
LinkedIn profile: http://www.linkedin.com/in/arnoutvandecappelle
GPG fingerprint: 7493 020B C7E3 8618 8DEC 222C 82EB F404 F9AC 0DDF
next prev parent reply other threads:[~2016-02-01 21:50 UTC|newest]
Thread overview: 27+ messages / expand[flat|nested] mbox.gz Atom feed top
2016-02-01 15:53 [Buildroot] [PATCH v6 00/13] Relocatable SDK / build machine leaks Samuel Martin
2016-02-01 15:53 ` [Buildroot] [PATCH v6 01/13] package/linux-headers: cleanup installation Samuel Martin
2016-02-01 17:34 ` Thomas Petazzoni
2016-02-01 15:53 ` [Buildroot] [PATCH v6 02/13] core: use $(CURDIR) to set TOPDIR Samuel Martin
2016-02-01 18:51 ` Arnout Vandecappelle
2016-02-01 15:53 ` [Buildroot] [PATCH v6 03/13] core: re-enter make if $(CURDIR) or $(O) are not absolute canonical path Samuel Martin
2016-02-01 18:58 ` Arnout Vandecappelle
2016-02-03 20:15 ` Thomas Petazzoni
2016-02-01 15:53 ` [Buildroot] [PATCH v6 04/13] core: staging symlink uses a relative path when possible Samuel Martin
2016-02-01 17:38 ` Thomas Petazzoni
2016-02-01 15:53 ` [Buildroot] [PATCH v6 05/13] core: make staging *-config scripts relocatable Samuel Martin
2016-02-01 20:18 ` Arnout Vandecappelle
2016-02-01 15:53 ` [Buildroot] [PATCH v6 06/13] core: make host " Samuel Martin
2016-02-01 18:45 ` Thomas Petazzoni
2016-02-01 20:01 ` Arnout Vandecappelle
2016-02-01 20:03 ` Arnout Vandecappelle
2016-02-01 15:53 ` [Buildroot] [PATCH v6 07/13] support/scripts: add fix-rpath script + a bunch of helpers Samuel Martin
2016-02-01 21:50 ` Arnout Vandecappelle [this message]
2016-02-01 15:53 ` [Buildroot] [PATCH v6 08/13] core: add HOST_SANITIZE_RPATH_HOOK to TARGET_FINALIZE_HOOKS Samuel Martin
2016-02-02 17:46 ` Arnout Vandecappelle
2016-02-01 15:53 ` [Buildroot] [PATCH v6 09/13] core: add {TARGET, STAGING}_SANITIZE_RPATH_HOOK " Samuel Martin
2016-02-02 18:13 ` Arnout Vandecappelle
2016-02-01 15:53 ` [Buildroot] [PATCH v6 10/13] package/speex: remove no longer needed hook Samuel Martin
2016-02-02 18:17 ` Arnout Vandecappelle
2016-02-01 15:53 ` [Buildroot] [PATCH v6 11/13] support/scripts: update check-host-rpath to use the shell helpers Samuel Martin
2016-02-01 15:53 ` [Buildroot] [PATCH v6 12/13] support/scripts: add check-host-leaks script + all needed helpers Samuel Martin
2016-02-01 15:53 ` [Buildroot] [PATCH v6 13/13] core: add check-leaks-in-{target, host, staging} targets Samuel Martin
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=56AFD321.5000006@mind.be \
--to=arnout@mind.be \
--cc=buildroot@busybox.net \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.