Tarantool development patches archive
 help / color / mirror / Atom feed
From: imeevma@tarantool.org
To: korablev@tarantool.org
Cc: tarantool-patches@freelists.org
Subject: [tarantool-patches] [PATCH v3 7/9] sql: rework four semantic errors
Date: Sat,  2 Mar 2019 16:08:00 +0300	[thread overview]
Message-ID: <68603a625edc2f90ae0533b0462575e4818bb1da.1551530224.git.imeevma@gmail.com> (raw)
In-Reply-To: <cover.1551530224.git.imeevma@gmail.com>

This patch reworks four semantic errors.

Part of #3965
---
 src/box/errcode.h                                  |  4 ++++
 src/box/sql/build.c                                |  4 +++-
 src/box/sql/expr.c                                 | 28 ++++++++++++----------
 src/box/sql/parse.y                                | 16 +++++++++----
 src/box/sql/select.c                               |  5 +++-
 src/box/sql/where.c                                |  4 +++-
 test/box/misc.result                               |  4 ++++
 test/sql-tap/gh-2549-many-columns.test.lua         |  2 +-
 test/sql-tap/gh2548-select-compound-limit.test.lua |  2 +-
 test/sql-tap/hexlit.test.lua                       |  4 ++--
 test/sql-tap/join.test.lua                         | 18 +++++++-------
 test/sql-tap/join3.test.lua                        |  2 +-
 test/sql-tap/select7.test.lua                      |  2 +-
 test/sql-tap/where7.test.lua                       |  2 +-
 test/sql/gh-2347-max-int-literals.result           |  6 +++--
 test/sql/integer-overflow.result                   |  9 ++++---
 test/sql/iproto.result                             |  5 ++--
 17 files changed, 72 insertions(+), 45 deletions(-)

diff --git a/src/box/errcode.h b/src/box/errcode.h
index 057a6d3..06f7a63 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -242,6 +242,10 @@ struct errcode_record {
 	/*187 */_(ER_INDEX_DEF,			"%s prohibited in an index definition") \
 	/*188 */_(ER_CHECK_CONSTRAINT_DEF,	"%s prohibited in prohibited in a CHECK constraint definition") \
 	/*189 */_(ER_PRIMARY_KEY_DEF,		"Expressions are prohibited in a primary key definition") \
+	/*190 */_(ER_COLUMN_COUNT_MAX,		"Failed to create space '%s': space column count %d exceeds the limit (%d)") \
+	/*191 */_(ER_HEX_LITERAL_MAX,		"Hex literal %s%s length %d exceeds the supported limit (%d)") \
+	/*192 */_(ER_INT_LITERAL_MAX,		"Integer literal %s%s exceeds the supported range %lld - %lld") \
+	/*193 */_(ER_SQL_PARSER_LIMIT,		"%s%.*s %d exceeds the limit (%d)") \
 
 /*
  * !IMPORTANT! Please follow instructions at start of the file
diff --git a/src/box/sql/build.c b/src/box/sql/build.c
index 651e02a..64d2690 100644
--- a/src/box/sql/build.c
+++ b/src/box/sql/build.c
@@ -436,7 +436,9 @@ sqlAddColumn(Parse * pParse, Token * pName, struct type_def *type_def)
 
 #if SQL_MAX_COLUMN
 	if ((int)def->field_count + 1 > db->aLimit[SQL_LIMIT_COLUMN]) {
-		sqlErrorMsg(pParse, "too many columns on %s", def->name);
+		diag_set(ClientError, ER_COLUMN_COUNT_MAX, def->name,
+			 def->field_count + 1, db->aLimit[SQL_LIMIT_COLUMN]);
+		pParse->is_aborted = true;
 		return;
 	}
 #endif
diff --git a/src/box/sql/expr.c b/src/box/sql/expr.c
index 7ea344b..ffcb455 100644
--- a/src/box/sql/expr.c
+++ b/src/box/sql/expr.c
@@ -730,9 +730,9 @@ sqlExprCheckHeight(Parse * pParse, int nHeight)
 	int rc = SQL_OK;
 	int mxHeight = pParse->db->aLimit[SQL_LIMIT_EXPR_DEPTH];
 	if (nHeight > mxHeight) {
-		sqlErrorMsg(pParse,
-				"Expression tree is too large (maximum depth %d)",
-				mxHeight);
+		diag_set(ClientError, ER_SQL_PARSER_LIMIT, "Number of nodes "\
+			 "in expression tree", 0, "", nHeight, mxHeight);
+		pParse->is_aborted = true;
 		rc = SQL_ERROR;
 	}
 	return rc;
@@ -1174,9 +1174,9 @@ sqlExprAssignVarNumber(Parse * pParse, Expr * pExpr, u32 n)
 			testcase(i == SQL_BIND_PARAMETER_MAX - 1);
 			testcase(i == SQL_BIND_PARAMETER_MAX);
 			if (!is_ok || i < 1 || i > SQL_BIND_PARAMETER_MAX) {
-				sqlErrorMsg(pParse,
-						"variable number must be between $1 and $%d",
-						SQL_BIND_PARAMETER_MAX);
+				diag_set(ClientError, ER_SQL_BIND_PARAMETER_MAX,
+					 SQL_BIND_PARAMETER_MAX);
+				pParse->is_aborted = true;
 				return;
 			}
 			if (x > pParse->nVar) {
@@ -1204,7 +1204,9 @@ sqlExprAssignVarNumber(Parse * pParse, Expr * pExpr, u32 n)
 	}
 	pExpr->iColumn = x;
 	if (x > SQL_BIND_PARAMETER_MAX) {
-		sqlErrorMsg(pParse, "too many SQL variables");
+		diag_set(ClientError, ER_SQL_BIND_PARAMETER_MAX,
+			 SQL_BIND_PARAMETER_MAX);
+		pParse->is_aborted = true;
 	}
 }
 
@@ -3314,15 +3316,15 @@ expr_code_int(struct Parse *parse, struct Expr *expr, bool is_neg,
 		int c = sql_dec_or_hex_to_i64(z, &value);
 		if (c == 1 || (c == 2 && !is_neg) ||
 		    (is_neg && value == SMALLEST_INT64)) {
+			const char *sign = is_neg ? "-" : "";
 			if (sql_strnicmp(z, "0x", 2) == 0) {
-				sqlErrorMsg(parse,
-						"hex literal too big: %s%s",
-						is_neg ? "-" : "", z);
+				diag_set(ClientError, ER_HEX_LITERAL_MAX, sign,
+					 z, strlen(z) - 2, 16);
 			} else {
-				sqlErrorMsg(parse,
-						"oversized integer: %s%s",
-						is_neg ? "-" : "", z);
+				diag_set(ClientError, ER_INT_LITERAL_MAX, sign,
+					 z, INT64_MIN, INT64_MAX);
 			}
+			parse->is_aborted = true;
 		} else {
 			if (is_neg)
 				value = c == 2 ? SMALLEST_INT64 : -value;
diff --git a/src/box/sql/parse.y b/src/box/sql/parse.y
index b8c0515..b69f059 100644
--- a/src/box/sql/parse.y
+++ b/src/box/sql/parse.y
@@ -410,9 +410,9 @@ cmd ::= select(X).  {
         (mxSelect = pParse->db->aLimit[SQL_LIMIT_COMPOUND_SELECT])>0 &&
         cnt>mxSelect
       ){
-        sqlErrorMsg(pParse, "Too many UNION or EXCEPT or INTERSECT "
-                        "operations (limit %d is set)",
-                        pParse->db->aLimit[SQL_LIMIT_COMPOUND_SELECT]);
+        diag_set(ClientError, ER_SQL_PARSER_LIMIT, "The number of UNION or "\
+                 "EXCEPT or INTERSECT operations", 0, "", cnt, mxSelect);
+        pParse->is_aborted = true;
       }
     }
   }
@@ -930,7 +930,10 @@ expr(A) ::= CAST(X) LP expr(E) AS typedef(T) RP(Y). {
 %endif  SQL_OMIT_CAST
 expr(A) ::= id(X) LP distinct(D) exprlist(Y) RP(E). {
   if( Y && Y->nExpr>pParse->db->aLimit[SQL_LIMIT_FUNCTION_ARG] ){
-    sqlErrorMsg(pParse, "too many arguments on function %T", &X);
+    diag_set(ClientError, ER_SQL_PARSER_LIMIT, "Number of "\
+             "arguments to function ", X.n, X.z, Y->nExpr,
+             pParse->db->aLimit[SQL_LIMIT_FUNCTION_ARG]);
+    pParse->is_aborted = true;
   }
   A.pExpr = sqlExprFunction(pParse, Y, &X);
   spanSet(&A,&X,&E);
@@ -944,7 +947,10 @@ type_func(A) ::= DATETIME(A) .
 type_func(A) ::= CHAR(A) .
 expr(A) ::= type_func(X) LP distinct(D) exprlist(Y) RP(E). {
   if( Y && Y->nExpr>pParse->db->aLimit[SQL_LIMIT_FUNCTION_ARG] ){
-    sqlErrorMsg(pParse, "too many arguments on function %T", &X);
+    diag_set(ClientError, ER_SQL_PARSER_LIMIT, "Number of "\
+             "arguments to function ", X.n, X.z, Y->nExpr,
+             pParse->db->aLimit[SQL_LIMIT_FUNCTION_ARG]);
+    pParse->is_aborted = true;
   }
   A.pExpr = sqlExprFunction(pParse, Y, &X);
   spanSet(&A,&X,&E);
diff --git a/src/box/sql/select.c b/src/box/sql/select.c
index 66cbc73..30ba947 100644
--- a/src/box/sql/select.c
+++ b/src/box/sql/select.c
@@ -5063,7 +5063,10 @@ selectExpander(Walker * pWalker, Select * p)
 	}
 #if SQL_MAX_COLUMN
 	if (p->pEList && p->pEList->nExpr > db->aLimit[SQL_LIMIT_COLUMN]) {
-		sqlErrorMsg(pParse, "too many columns in result set");
+		diag_set(ClientError, ER_SQL_PARSER_LIMIT, "The number of "\
+			 "columns in result set", 0, "", p->pEList->nExpr,
+			 db->aLimit[SQL_LIMIT_COLUMN]);
+		pParse->is_aborted = true;
 		return WRC_Abort;
 	}
 #endif
diff --git a/src/box/sql/where.c b/src/box/sql/where.c
index cf70e06..33885d0 100644
--- a/src/box/sql/where.c
+++ b/src/box/sql/where.c
@@ -4286,7 +4286,9 @@ sqlWhereBegin(Parse * pParse,	/* The parser context */
 	 */
 	testcase(pTabList->nSrc == BMS);
 	if (pTabList->nSrc > BMS) {
-		sqlErrorMsg(pParse, "at most %d tables in a join", BMS);
+		diag_set(ClientError, ER_SQL_PARSER_LIMIT, "The number of "\
+			 "tables in a join", 0, "", pTabList->nSrc, BMS);
+		pParse->is_aborted = true;
 		return 0;
 	}
 
diff --git a/test/box/misc.result b/test/box/misc.result
index a3bc7b7..2bb6613 100644
--- a/test/box/misc.result
+++ b/test/box/misc.result
@@ -518,6 +518,10 @@ t;
   187: box.error.INDEX_DEF
   188: box.error.CHECK_CONSTRAINT_DEF
   189: box.error.PRIMARY_KEY_DEF
+  190: box.error.COLUMN_COUNT_MAX
+  191: box.error.HEX_LITERAL_MAX
+  192: box.error.INT_LITERAL_MAX
+  193: box.error.SQL_PARSER_LIMIT
 ...
 test_run:cmd("setopt delimiter ''");
 ---
diff --git a/test/sql-tap/gh-2549-many-columns.test.lua b/test/sql-tap/gh-2549-many-columns.test.lua
index 3de4d67..ed8c9d9 100755
--- a/test/sql-tap/gh-2549-many-columns.test.lua
+++ b/test/sql-tap/gh-2549-many-columns.test.lua
@@ -35,7 +35,7 @@ test:do_catchsql_test(
 	"columns-1.2",
 	fail_statement, {
 		-- <columns-1.2>
-		1, "too many columns on T2"
+		1, "Failed to create space 'T2': space column count 2001 exceeds the limit (2000)"
 		-- <columns-1.2>
 	})
 
diff --git a/test/sql-tap/gh2548-select-compound-limit.test.lua b/test/sql-tap/gh2548-select-compound-limit.test.lua
index 5494a66..e8c8d95 100755
--- a/test/sql-tap/gh2548-select-compound-limit.test.lua
+++ b/test/sql-tap/gh2548-select-compound-limit.test.lua
@@ -58,7 +58,7 @@ test:do_catchsql_test(
     "gh2548-select-compound-limit-2",
     select_string_last, {
         -- <gh2548-select-compound-limit-2>
-        1, "Too many UNION or EXCEPT or INTERSECT operations (limit 30 is set)"
+        1, "The number of UNION or EXCEPT or INTERSECT operations 31 exceeds the limit (30)"
         -- </gh2548-select-compound-limit-2>
     })
 
diff --git a/test/sql-tap/hexlit.test.lua b/test/sql-tap/hexlit.test.lua
index 158eda7..288d823 100755
--- a/test/sql-tap/hexlit.test.lua
+++ b/test/sql-tap/hexlit.test.lua
@@ -107,7 +107,7 @@ test:do_catchsql_test(
         SELECT 0x10000000000000000;
     ]], {
         -- <hexlist-400>
-        1, "hex literal too big: 0x10000000000000000"
+        1, "Hex literal 0x10000000000000000 length 17 exceeds the supported limit (16)"
         -- </hexlist-400>
     })
 
@@ -119,7 +119,7 @@ test:do_catchsql_test(
         INSERT INTO t1 VALUES(1+0x10000000000000000);
     ]], {
         -- <hexlist-410>
-        1, "hex literal too big: 0x10000000000000000"
+        1, "Hex literal 0x10000000000000000 length 17 exceeds the supported limit (16)"
         -- </hexlist-410>
     })
 
diff --git a/test/sql-tap/join.test.lua b/test/sql-tap/join.test.lua
index df272a9..ce0d9e9 100755
--- a/test/sql-tap/join.test.lua
+++ b/test/sql-tap/join.test.lua
@@ -1067,11 +1067,11 @@ end
 jointest("join-12.2", 30, {0, {1}})
 jointest("join-12.3", 63, {0, {1}})
 jointest("join-12.4", 64, {0, {1}})
-jointest("join-12.5", 65, {1, 'at most 64 tables in a join'})
-jointest("join-12.6", 66, {1, 'at most 64 tables in a join'})
-jointest("join-12.7", 127, {1, 'at most 64 tables in a join'})
-jointest("join-12.8", 128, {1, 'at most 64 tables in a join'})
-jointest("join-12.9", 1000, {1, 'at most 64 tables in a join'})
+jointest("join-12.5", 65, {1, 'The number of tables in a join 65 exceeds the limit (64)'})
+jointest("join-12.6", 66, {1, 'The number of tables in a join 66 exceeds the limit (64)'})
+jointest("join-12.7", 127, {1, 'The number of tables in a join 127 exceeds the limit (64)'})
+jointest("join-12.8", 128, {1, 'The number of tables in a join 128 exceeds the limit (64)'})
+jointest("join-12.9", 1000, {1, 'The number of tables in a join 1000 exceeds the limit (64)'})
 -- If sql is built with sql_MEMDEBUG, then the huge number of realloc()
 -- calls made by the following test cases are too time consuming to run.
 -- Without sql_MEMDEBUG, realloc() is fast enough that these are not
@@ -1079,10 +1079,10 @@ jointest("join-12.9", 1000, {1, 'at most 64 tables in a join'})
 --if X(0, "X!capable", [["pragma&&compileoption_diags"]]) then
 --    if X(703, "X!cmd", [=[["expr","[lsearch [db eval {PRAGMA compile_options}] MEMDEBUG]<0"]]=])
 -- then
-jointest("join-12.10", 65534, {1, 'at most 64 tables in a join'})
-jointest("join-12.11", 65535, {1, 'at most 64 tables in a join'})
-jointest("join-12.12", 65536, {1, 'at most 64 tables in a join'})
-jointest("join-12.13", 65537, {1, 'at most 64 tables in a join'})
+jointest("join-12.10", 65534, {1, 'The number of tables in a join 65534 exceeds the limit (64)'})
+jointest("join-12.11", 65535, {1, 'The number of tables in a join 65535 exceeds the limit (64)'})
+jointest("join-12.12", 65536, {1, 'The number of tables in a join 65536 exceeds the limit (64)'})
+jointest("join-12.13", 65537, {1, 'The number of tables in a join 65537 exceeds the limit (64)'})
 --    end
 --end
 
diff --git a/test/sql-tap/join3.test.lua b/test/sql-tap/join3.test.lua
index 6b822de..91118dc 100755
--- a/test/sql-tap/join3.test.lua
+++ b/test/sql-tap/join3.test.lua
@@ -85,7 +85,7 @@ test:do_test(
         return test:catchsql(sql)
     end, {
         -- <join3-3.1>
-        1, "at most "..bitmask_size.." tables in a join"
+        1, "The number of tables in a join 65 exceeds the limit (64)"
         -- </join3-3.1>
     })
 
diff --git a/test/sql-tap/select7.test.lua b/test/sql-tap/select7.test.lua
index 4029c20..7037d20 100755
--- a/test/sql-tap/select7.test.lua
+++ b/test/sql-tap/select7.test.lua
@@ -179,7 +179,7 @@ test:do_catchsql_test(
     "select7-6.2",
     sql, {
         -- <select7-6.2>
-        1, "Too many UNION or EXCEPT or INTERSECT operations (limit 30 is set)"
+        1, "The number of UNION or EXCEPT or INTERSECT operations 33 exceeds the limit (30)"
         -- </select7-6.2>
     })
 
diff --git a/test/sql-tap/where7.test.lua b/test/sql-tap/where7.test.lua
index 2e6f116..ecd0d24 100755
--- a/test/sql-tap/where7.test.lua
+++ b/test/sql-tap/where7.test.lua
@@ -325,7 +325,7 @@ test:do_test(
         end
         return test:catchsql(sql)
     end, {
-        1, "Expression tree is too large (maximum depth 200)"
+        1, "Number of nodes in expression tree 201 exceeds the limit (200)"
     })
 
 test:do_test(
diff --git a/test/sql/gh-2347-max-int-literals.result b/test/sql/gh-2347-max-int-literals.result
index c289a80..b511440 100644
--- a/test/sql/gh-2347-max-int-literals.result
+++ b/test/sql/gh-2347-max-int-literals.result
@@ -20,9 +20,11 @@ box.sql.execute("select (-9223372036854775808)")
 ...
 box.sql.execute("select (9223372036854775808)")
 ---
-- error: 'oversized integer: 9223372036854775808'
+- error: Integer literal 9223372036854775808 exceeds the supported range -9223372036854775808
+    - 9223372036854775807
 ...
 box.sql.execute("select (-9223372036854775809)")
 ---
-- error: 'oversized integer: -9223372036854775809'
+- error: Integer literal -9223372036854775809 exceeds the supported range -9223372036854775808
+    - 9223372036854775807
 ...
diff --git a/test/sql/integer-overflow.result b/test/sql/integer-overflow.result
index 4754c04..09e864e 100644
--- a/test/sql/integer-overflow.result
+++ b/test/sql/integer-overflow.result
@@ -30,15 +30,18 @@ box.sql.execute('SELECT (9223372036854775807 + 1);')
 --
 box.sql.execute('SELECT 9223372036854775808;')
 ---
-- error: 'oversized integer: 9223372036854775808'
+- error: Integer literal 9223372036854775808 exceeds the supported range -9223372036854775808
+    - 9223372036854775807
 ...
 box.sql.execute('SELECT -9223372036854775809;')
 ---
-- error: 'oversized integer: -9223372036854775809'
+- error: Integer literal -9223372036854775809 exceeds the supported range -9223372036854775808
+    - 9223372036854775807
 ...
 box.sql.execute('SELECT 9223372036854775808 - 1;')
 ---
-- error: 'oversized integer: 9223372036854775808'
+- error: Integer literal 9223372036854775808 exceeds the supported range -9223372036854775808
+    - 9223372036854775807
 ...
 -- Test that CAST may also leads to overflow.
 --
diff --git a/test/sql/iproto.result b/test/sql/iproto.result
index 938aea9..56099fa 100644
--- a/test/sql/iproto.result
+++ b/test/sql/iproto.result
@@ -363,7 +363,7 @@ sql = 'select '..string.rep('?, ', box.schema.SQL_BIND_PARAMETER_MAX)..'?'
 ...
 cn:execute(sql)
 ---
-- error: 'Failed to execute SQL statement: too many SQL variables'
+- error: 'Failed to execute SQL statement: SQL bind parameter limit reached: 65000'
 ...
 -- Try too many parameter values.
 sql = 'select ?'
@@ -571,8 +571,7 @@ cn:execute('select ?1, ?2, ?3', {1, 2, 3})
 ...
 cn:execute('select $name, $name2', {1, 2})
 ---
-- error: 'Failed to execute SQL statement: variable number must be between $1 and
-    $65000'
+- error: 'Failed to execute SQL statement: SQL bind parameter limit reached: 65000'
 ...
 parameters = {}
 ---
-- 
2.7.4

  parent reply	other threads:[~2019-03-02 13:08 UTC|newest]

Thread overview: 30+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2019-03-02 13:07 [tarantool-patches] [PATCH v3 0/9] sql: use diag_set() for errors in SQL imeevma
2019-03-02 13:07 ` [tarantool-patches] [PATCH v3 1/9] sql: rework syntax errors imeevma
2019-03-04 17:47   ` [tarantool-patches] " n.pettik
2019-03-05  8:31   ` Konstantin Osipov
2019-03-02 13:07 ` [tarantool-patches] [PATCH v3 2/9] sql: save SQL parser errors in diag_set() imeevma
2019-03-05  8:40   ` [tarantool-patches] " Konstantin Osipov
2019-03-05  9:06   ` n.pettik
2019-03-02 13:07 ` [tarantool-patches] [PATCH v3 3/9] sql: remove field nErr of struct Parse imeevma
2019-03-05  8:41   ` [tarantool-patches] " Konstantin Osipov
2019-03-05  9:06   ` n.pettik
2019-03-02 13:07 ` [tarantool-patches] [PATCH v3 4/9] sql: remove field rc " imeevma
2019-03-05  8:42   ` [tarantool-patches] " Konstantin Osipov
2019-03-05  9:06   ` n.pettik
2019-03-02 13:07 ` [tarantool-patches] [PATCH v3 5/9] sql: remove field zErrMsg " imeevma
2019-03-05  8:43   ` [tarantool-patches] " Konstantin Osipov
2019-03-05  9:06   ` n.pettik
2019-03-02 13:07 ` [tarantool-patches] [PATCH v3 6/9] sql: rework six syntax errors imeevma
2019-03-05  8:45   ` [tarantool-patches] " Konstantin Osipov
2019-03-05  9:07   ` n.pettik
2019-03-02 13:08 ` imeevma [this message]
2019-03-05  8:46   ` [tarantool-patches] Re: [PATCH v3 7/9] sql: rework four semantic errors Konstantin Osipov
2019-03-05  9:16   ` n.pettik
2019-03-02 13:08 ` [tarantool-patches] [PATCH v3 8/9] sql: rework three errors of "unsupported" type imeevma
2019-03-05  8:47   ` [tarantool-patches] " Konstantin Osipov
2019-03-05  9:34   ` n.pettik
2019-03-05  9:43     ` Konstantin Osipov
2019-03-02 13:08 ` [tarantool-patches] [PATCH v3 9/9] sql: remove sqlErrorMsg() imeevma
2019-03-05  8:48   ` [tarantool-patches] " Konstantin Osipov
2019-03-05 12:16   ` n.pettik
2019-03-05 15:44     ` Konstantin Osipov

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=68603a625edc2f90ae0533b0462575e4818bb1da.1551530224.git.imeevma@gmail.com \
    --to=imeevma@tarantool.org \
    --cc=korablev@tarantool.org \
    --cc=tarantool-patches@freelists.org \
    --subject='Re: [tarantool-patches] [PATCH v3 7/9] sql: rework four semantic errors' \
    /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

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox