From: David Malcolm <dmalcolm@redhat.com>
To: gcc-patches@gcc.gnu.org, binutils@sourceware.org
Cc: Nick Clifton <nickc@redhat.com>,
Simon Sobisch <simonsobisch@gnu.org>,
David Malcolm <dmalcolm@redhat.com>
Subject: [PATCH 3/5] libdiagnostics v2: add C++ wrapper API
Date: Tue, 21 Nov 2023 17:20:16 -0500 [thread overview]
Message-ID: <20231121222019.646253-4-dmalcolm@redhat.com> (raw)
In-Reply-To: <20231121222019.646253-1-dmalcolm@redhat.com>
This is new in v2: a C++ wrapper API that provides some syntactic sugar for
calling into libdiagnostics.{h,so}.
I've been "eating my own dogfood" with this by using it to write a simple
client that reads a SARIF file and dumps it using the text sink:
https://github.com/davidmalcolm/libdiagnostics-sarif-dump
gcc/ChangeLog:
* libdiagnostics++.h: New file.
gcc/testsuite/ChangeLog:
* libdiagnostics.dg/libdiagnostics.exp: Add .cc tests.
* libdiagnostics.dg/test-error-with-note.cc: New test.
* libdiagnostics.dg/test-error.cc: New test.
* libdiagnostics.dg/test-fix-it-hint.cc: New test.
* libdiagnostics.dg/test-helpers++.h: New header.
* libdiagnostics.dg/test-labelled-ranges.cc: New test.
---
gcc/libdiagnostics++.h | 378 ++++++++++++++++++
.../libdiagnostics.dg/libdiagnostics.exp | 8 +-
.../libdiagnostics.dg/test-error-with-note.cc | 47 +++
gcc/testsuite/libdiagnostics.dg/test-error.cc | 40 ++
.../libdiagnostics.dg/test-fix-it-hint.cc | 44 ++
.../libdiagnostics.dg/test-helpers++.h | 28 ++
.../libdiagnostics.dg/test-labelled-ranges.cc | 43 ++
7 files changed, 584 insertions(+), 4 deletions(-)
create mode 100644 gcc/libdiagnostics++.h
create mode 100644 gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc
create mode 100644 gcc/testsuite/libdiagnostics.dg/test-error.cc
create mode 100644 gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc
create mode 100644 gcc/testsuite/libdiagnostics.dg/test-helpers++.h
create mode 100644 gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc
diff --git a/gcc/libdiagnostics++.h b/gcc/libdiagnostics++.h
new file mode 100644
index 000000000000..8f412b07aa78
--- /dev/null
+++ b/gcc/libdiagnostics++.h
@@ -0,0 +1,378 @@
+/* A C++ wrapper API around libdiagnostics.h for emitting diagnostics.
+ Copyright (C) 2023 Free Software Foundation, Inc.
+
+This file is part of GCC.
+
+GCC 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 3, or (at your option)
+any later version.
+
+GCC 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 GCC; see the file COPYING3. If not see
+<http://www.gnu.org/licenses/>. */
+
+#ifndef LIBDIAGNOSTICSPP_H
+#define LIBDIAGNOSTICSPP_H
+
+#include "libdiagnostics.h"
+
+namespace libdiagnostics {
+
+typedef diagnostic_line_num_t line_num_t;
+typedef diagnostic_column_num_t column_num_t;
+
+class file;
+class physical_location;
+class logical_location;
+class group;
+class manager;
+class diagnostic;
+
+/* Wrapper around a const diagnostic_file *. */
+
+class file
+{
+public:
+ file (const diagnostic_file *file) : m_inner (file) {}
+
+ const diagnostic_file * const m_inner;
+};
+
+/* Wrapper around a const diagnostic_physical_location *. */
+
+class physical_location
+{
+public:
+ physical_location (const diagnostic_physical_location *location)
+ : m_inner (location)
+ {}
+
+ const diagnostic_physical_location *m_inner;
+};
+
+/* Wrapper around a const diagnostic_logical_location *. */
+
+class logical_location
+{
+public:
+ logical_location () : m_inner (nullptr) {}
+
+ logical_location (const diagnostic_logical_location *logical_loc)
+ : m_inner (logical_loc)
+ {}
+
+ const diagnostic_logical_location * const m_inner;
+};
+
+/* RAII class for starting/ending a group within a diagnostic_manager. */
+
+class group
+{
+public:
+ group (manager &mgr);
+ ~group ();
+
+private:
+ manager &m_mgr;
+};
+
+/* Wrapper around a diagnostic *. */
+
+class diagnostic
+{
+public:
+ diagnostic (::diagnostic *d) : m_inner (d) {}
+
+ void
+ set_cwe (unsigned cwe_id);
+
+ void
+ set_location (physical_location loc);
+
+ void
+ add_location_with_label (physical_location loc,
+ const char *text);
+
+ void
+ set_logical_location (logical_location loc);
+
+ void
+ add_fix_it_hint_insert_before (physical_location loc,
+ const char *addition);
+ void
+ add_fix_it_hint_insert_after (physical_location loc,
+ const char *addition);
+ void
+ add_fix_it_hint_replace (physical_location loc,
+ const char *replacement);
+ void
+ add_fix_it_hint_delete (physical_location loc);
+
+ void
+ finish (const char *fmt, ...)
+ LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
+ LIBDIAGNOSTICS_PARAM_GCC_FORMAT_STRING (2, 3);
+
+ ::diagnostic * const m_inner;
+};
+
+/* Wrapper around ownership of a diagnostic_manager *. */
+
+class manager
+{
+public:
+ manager ()
+ : m_inner (diagnostic_manager_new ())
+ {
+ }
+ ~manager ()
+ {
+ diagnostic_manager_release (m_inner);
+ }
+
+ void
+ add_text_sink (FILE *dst_stream,
+ enum diagnostic_colorize colorize)
+ {
+ diagnostic_manager_add_text_sink (m_inner, dst_stream, colorize);
+ }
+
+ void
+ add_sarif_sink (FILE *dst_stream,
+ enum diagnostic_sarif_version version)
+ {
+ diagnostic_manager_add_sarif_sink (m_inner, dst_stream, version);
+ }
+
+ void
+ write_patch (FILE *dst_stream)
+ {
+ diagnostic_manager_write_patch (m_inner, dst_stream);
+ }
+
+ /* Location management. */
+
+ file
+ new_file (const char *name,
+ const char *sarif_source_language)
+ LIBDIAGNOSTICS_PARAM_MUST_BE_NON_NULL (2)
+ LIBDIAGNOSTICS_PARAM_CAN_BE_NULL (3);
+
+ void
+ debug_dump (file f,
+ FILE *out);
+
+ physical_location
+ new_location_from_file_and_line (file f, diagnostic_line_num_t line_num);
+
+ physical_location
+ new_location_from_file_line_column (file f,
+ line_num_t line_num,
+ column_num_t column_num);
+
+ physical_location
+ new_location_from_range (physical_location loc_caret,
+ physical_location loc_start,
+ physical_location loc_end);
+
+ void
+ debug_dump (physical_location loc,
+ FILE *out);
+
+ logical_location
+ new_logical_location (enum diagnostic_logical_location_kind_t kind,
+ logical_location parent,
+ const char *short_name,
+ const char *fully_qualified_name,
+ const char *decorated_name);
+
+ void
+ debug_dump (logical_location loc,
+ FILE *out);
+
+ diagnostic
+ begin_diagnostic (enum diagnostic_level level);
+
+
+ diagnostic_manager * const m_inner;
+};
+
+// Implementation
+
+// class group
+
+inline
+group::group (manager &mgr)
+: m_mgr (mgr)
+{
+ diagnostic_manager_begin_group (m_mgr.m_inner);
+}
+
+inline
+group::~group ()
+{
+ diagnostic_manager_end_group (m_mgr.m_inner);
+}
+
+// class diagnostic
+
+inline void
+diagnostic::set_cwe (unsigned cwe_id)
+{
+ diagnostic_set_cwe (m_inner, cwe_id);
+}
+
+inline void
+diagnostic::set_location (physical_location loc)
+{
+ diagnostic_set_location (m_inner, loc.m_inner);
+}
+
+inline void
+diagnostic::add_location_with_label (physical_location loc,
+ const char *text)
+{
+ diagnostic_add_location_with_label (m_inner, loc.m_inner, text);
+}
+
+inline void
+diagnostic::set_logical_location (logical_location loc)
+{
+ diagnostic_set_logical_location (m_inner, loc.m_inner);
+}
+
+inline void
+diagnostic::add_fix_it_hint_insert_before (physical_location loc,
+ const char *addition)
+{
+ diagnostic_add_fix_it_hint_insert_before (m_inner,
+ loc.m_inner,
+ addition);
+}
+
+inline void
+diagnostic::add_fix_it_hint_insert_after (physical_location loc,
+ const char *addition)
+{
+ diagnostic_add_fix_it_hint_insert_after (m_inner,
+ loc.m_inner,
+ addition);
+}
+
+inline void
+diagnostic::add_fix_it_hint_replace (physical_location loc,
+ const char *replacement)
+{
+ diagnostic_add_fix_it_hint_replace (m_inner,
+ loc.m_inner,
+ replacement);
+}
+
+inline void
+diagnostic::add_fix_it_hint_delete (physical_location loc)
+{
+ diagnostic_add_fix_it_hint_delete (m_inner,
+ loc.m_inner);
+}
+
+inline void
+diagnostic::finish (const char *fmt, ...)
+{
+ va_list ap;
+ va_start (ap, fmt);
+ diagnostic_finish_va (m_inner, fmt, &ap);
+ va_end (ap);
+}
+
+// class manager
+
+inline file
+manager::new_file (const char *name,
+ const char *sarif_source_language)
+{
+ return file
+ (diagnostic_manager_new_file (m_inner, name, sarif_source_language));
+}
+
+inline physical_location
+manager::new_location_from_file_and_line (file f,
+ diagnostic_line_num_t line_num)
+{
+ return physical_location
+ (diagnostic_manager_new_location_from_file_and_line (m_inner,
+ f.m_inner,
+ line_num));
+}
+
+inline physical_location
+manager::new_location_from_file_line_column (file f,
+ line_num_t line_num,
+ column_num_t column_num)
+{
+ return physical_location
+ (diagnostic_manager_new_location_from_file_line_column (m_inner,
+ f.m_inner,
+ line_num,
+ column_num));
+}
+
+inline physical_location
+manager::new_location_from_range (physical_location loc_caret,
+ physical_location loc_start,
+ physical_location loc_end)
+{
+ return physical_location
+ (diagnostic_manager_new_location_from_range (m_inner,
+ loc_caret.m_inner,
+ loc_start.m_inner,
+ loc_end.m_inner));
+}
+
+inline void
+manager::debug_dump (physical_location loc,
+ FILE *out)
+{
+ diagnostic_manager_debug_dump_location (m_inner,
+ loc.m_inner,
+ out);
+}
+inline logical_location
+manager::new_logical_location (enum diagnostic_logical_location_kind_t kind,
+ logical_location parent,
+ const char *short_name,
+ const char *fully_qualified_name,
+ const char *decorated_name)
+{
+ return logical_location
+ (diagnostic_manager_new_logical_location (m_inner,
+ kind,
+ parent.m_inner,
+ short_name,
+ fully_qualified_name,
+ decorated_name));
+}
+
+inline void
+manager::debug_dump (logical_location loc,
+ FILE *out)
+{
+ diagnostic_manager_debug_dump_logical_location (m_inner,
+ loc.m_inner,
+ out);
+}
+
+inline diagnostic
+manager::begin_diagnostic (enum diagnostic_level level)
+{
+ return diagnostic (diagnostic_begin (m_inner, level));
+}
+
+} // namespace libdiagnostics
+
+#endif // #ifndef LIBDIAGNOSTICSPP_H
diff --git a/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp b/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp
index bd50a5568a73..bcceb669d3ff 100644
--- a/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp
+++ b/gcc/testsuite/libdiagnostics.dg/libdiagnostics.exp
@@ -345,10 +345,10 @@ dg-init
# Gather a list of all tests.
-# C tests within the testsuite: gcc/testsuite/libdiagnostics.dg/test-*.c
-set tests [find $srcdir/$subdir test-*.c]
-
-set tests [lsort $tests]
+# C and C++ tests within the testsuite: gcc/testsuite/libdiagnostics.dg/test-*.{c,c++}
+set c_tests [find $srcdir/$subdir test-*.c]
+set cxx_tests [find $srcdir/$subdir test-*.cc]
+set tests [concat $c_tests $cxx_tests]
verbose "tests: $tests"
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc
new file mode 100644
index 000000000000..e20b9e04d6b1
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-error-with-note.cc
@@ -0,0 +1,47 @@
+/* C++ example of emitting an error with an associated note.
+
+ Intended output is similar to:
+
+PATH/test-error-with-note.c:6: error: can't find 'foo'
+ 6 | PRINT "hello world!";
+ | ^~~~~~~~~~~~
+PATH/test-error-with-note.c:6: note: have you looked behind the couch?
+
+ along with the equivalent in SARIF. */
+
+#include "libdiagnostics++.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+ libdiagnostics::manager mgr;
+
+ mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+ mgr.add_sarif_sink (stderr, DIAGNOSTIC_SARIF_VERSION_2_1_0);
+
+ auto file = mgr.new_file (__FILE__, "c");
+ auto loc_start = mgr.new_location_from_file_line_column (file, line_num, 8);
+ auto loc_end = mgr.new_location_from_file_line_column (file, line_num, 19);
+ auto loc_range = mgr.new_location_from_range (loc_start,
+ loc_start,
+ loc_end);
+
+ libdiagnostics::group g (mgr);
+
+ auto err (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR));
+ err.set_location (loc_range);
+ err.finish ("can't find %qs", "foo");
+
+ auto note = mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_NOTE);
+ note.set_location (loc_range);
+ note.finish ("have you looked behind the couch?");
+
+ return 0;
+};
diff --git a/gcc/testsuite/libdiagnostics.dg/test-error.cc b/gcc/testsuite/libdiagnostics.dg/test-error.cc
new file mode 100644
index 000000000000..019307815bfb
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-error.cc
@@ -0,0 +1,40 @@
+/* C++ example of emitting an error.
+
+ Intended output is similar to:
+
+PATH/test-error.cc:6: error: can't find 'foo'
+ 6 | PRINT "hello world!";
+ | ^~~~~~~~~~~~
+
+ along with the equivalent in SARIF. */
+
+#include "libdiagnostics++.h"
+
+/*
+_________111111111122
+123456789012345678901
+PRINT "hello world!";
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+ libdiagnostics::manager mgr;
+
+ mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+ mgr.add_sarif_sink (stderr, DIAGNOSTIC_SARIF_VERSION_2_1_0);
+
+ auto file = mgr.new_file (__FILE__, "c");
+ auto loc_start = mgr.new_location_from_file_line_column (file, line_num, 8);
+ auto loc_end = mgr.new_location_from_file_line_column (file, line_num, 19);
+ auto loc_range = mgr.new_location_from_range (loc_start,
+ loc_start,
+ loc_end);
+
+ libdiagnostics::diagnostic d (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR));
+ d.set_location (loc_range);
+ d.finish ("can't find %qs", "foo");
+
+ return 0;
+};
diff --git a/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc
new file mode 100644
index 000000000000..6af2641f2804
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-fix-it-hint.cc
@@ -0,0 +1,44 @@
+/* C++ example of a fix-it hint, including patch generation.
+
+ Intended output is similar to:
+
+PATH/test-fix-it-hint.cc:19: error: unknown field 'colour'; did you mean 'color'
+ 19 | return p->colour;
+ | ^~~~~~
+ | color
+
+ along with the equivalent in SARIF, and a generated patch (on stderr) to
+ make the change. */
+
+#include "libdiagnostics++.h"
+#include "test-helpers++.h"
+
+/*
+_________11111111112
+12345678901234567890
+ return p->colour;
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+ libdiagnostics::manager mgr;
+
+ mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+ mgr.add_sarif_sink (stderr, DIAGNOSTIC_SARIF_VERSION_2_1_0);
+
+ auto file = mgr.new_file (__FILE__, "c");
+ auto loc_token = make_range (mgr, file, line_num, 13, 18);
+
+ auto d = mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR);
+ d.set_location (loc_token);
+
+ d.add_fix_it_hint_replace (loc_token, "color");
+
+ d.finish ("unknown field %qs; did you mean %qs", "colour", "color");
+
+ mgr.write_patch (stderr);
+
+ return 0;
+}
diff --git a/gcc/testsuite/libdiagnostics.dg/test-helpers++.h b/gcc/testsuite/libdiagnostics.dg/test-helpers++.h
new file mode 100644
index 000000000000..c8ff2def1ffa
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-helpers++.h
@@ -0,0 +1,28 @@
+/* Common utility code shared between test cases. */
+
+#ifndef TEST_HELPERSPP_H
+#define TEST_HELPERSPP_H
+
+namespace libdiagnostics {
+
+inline physical_location
+make_range (manager &mgr,
+ file f,
+ line_num_t line_num,
+ column_num_t start_column,
+ column_num_t end_column)
+{
+ auto loc_start = mgr.new_location_from_file_line_column (f,
+ line_num,
+ start_column);
+ auto loc_end = mgr.new_location_from_file_line_column (f,
+ line_num,
+ end_column);
+ return mgr.new_location_from_range (loc_start,
+ loc_start,
+ loc_end);
+}
+
+} // namespace libdiagnostics
+
+#endif /* #ifndef TEST_HELPERSPP_H */
diff --git a/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc
new file mode 100644
index 000000000000..35ccf74e1529
--- /dev/null
+++ b/gcc/testsuite/libdiagnostics.dg/test-labelled-ranges.cc
@@ -0,0 +1,43 @@
+/* C++ example of multiple locations, with labelling of ranges.
+
+ Intended output is similar to:
+
+PATH/test-labelled-ranges.cc:9: error: mismatching types: 'int' and 'const char *'
+ 19 | 42 + "foo"
+ | ~~ ^ ~~~~~
+ | | |
+ | int const char *
+
+ along with the equivalent in SARIF. */
+
+#include "libdiagnostics++.h"
+#include "test-helpers++.h"
+
+/*
+_________11111111112
+12345678901234567890
+ 42 + "foo"
+*/
+const int line_num = __LINE__ - 2;
+
+int
+main ()
+{
+ libdiagnostics::manager mgr;
+
+ mgr.add_text_sink (stderr, DIAGNOSTIC_COLORIZE_IF_TTY);
+ mgr.add_sarif_sink (stderr, DIAGNOSTIC_SARIF_VERSION_2_1_0);
+
+ libdiagnostics::file file = mgr.new_file (__FILE__, "c");
+ auto loc_operator = mgr.new_location_from_file_line_column (file, line_num, 6);
+
+ auto d (mgr.begin_diagnostic (DIAGNOSTIC_LEVEL_ERROR));
+ d.set_location (loc_operator);
+ d.add_location_with_label (make_range (mgr, file, line_num, 3, 4),
+ "int");
+ d.add_location_with_label (make_range (mgr, file, line_num, 8, 12),
+ "const char *");
+ d.finish ("mismatching types: %qs and %qs", "int", "const char *");
+
+ return 0;
+}
--
2.26.3
next prev parent reply other threads:[~2023-11-21 22:20 UTC|newest]
Thread overview: 29+ messages / expand[flat|nested] mbox.gz Atom feed top
2023-11-06 22:29 [PATCH/RFC] libdiagnostics: a shared library for emitting diagnostics David Malcolm
2023-11-06 22:29 ` [PATCH 1/2] libdiagnostics: header and examples David Malcolm
2023-11-06 22:29 ` [PATCH 2/2] libdiagnostics: work-in-progress implementation David Malcolm
2023-11-07 7:54 ` Simon Sobisch
2023-11-07 14:59 ` David Malcolm
2023-11-07 15:35 ` Simon Sobisch
2023-11-06 22:29 ` [PATCH] binutils: experimental use of libdiagnostics in gas David Malcolm
2023-11-07 7:04 ` Simon Sobisch
2023-11-07 14:51 ` David Malcolm
2023-11-07 9:21 ` Clément Chigot
2023-11-07 14:09 ` David Malcolm
2023-11-07 15:57 ` Clément Chigot
2023-11-07 16:18 ` David Malcolm
2023-11-07 10:03 ` Jan Beulich
2023-11-07 14:32 ` David Malcolm
2023-11-07 14:59 ` Jan Beulich
2023-11-21 22:20 ` [PATCH 0/6] v2 of libdiagnostics David Malcolm
2023-11-21 22:20 ` [PATCH 1/5] libdiagnostics v2: header and examples David Malcolm
2023-11-21 22:20 ` [PATCH 2/5] libdiagnostics v2: work-in-progress implementation David Malcolm
2023-11-21 22:20 ` David Malcolm [this message]
2023-11-21 22:20 ` [PATCH 4/5] diagnostics: add diagnostic_context::get_location_text David Malcolm
2023-11-28 1:25 ` David Malcolm
2023-11-21 22:20 ` [PATCH 5/5] diagnostics: don't print annotation lines when there's no column info David Malcolm
2023-11-28 1:25 ` David Malcolm
2023-11-21 22:20 ` [PATCH] binutils: v2: experimental use of libdiagnostics in gas David Malcolm
2023-11-22 7:36 ` Jan Beulich
2023-11-21 22:35 ` [PATCH 0/6] v2 of libdiagnostics Simon Sobisch
2023-11-23 17:36 ` Pedro Alves
2024-01-27 23:28 ` Simon Sobisch
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=20231121222019.646253-4-dmalcolm@redhat.com \
--to=dmalcolm@redhat.com \
--cc=binutils@sourceware.org \
--cc=gcc-patches@gcc.gnu.org \
--cc=nickc@redhat.com \
--cc=simonsobisch@gnu.org \
/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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).