On 09/27/2014 11:50 PM, Marko Tiikkaja wrote:
Hi,

On 9/25/14, 3:56 PM, I wrote:
On 9/25/14 3:50 PM, Heikki Linnakangas wrote:
Are you planning to post the main patch rebased on top of this soon? As
in the next day or two? Otherwise I'll mark this as "Returned with
feedback" for this commitfest.

Yes.  With good luck I'll get you a rebased one today, otherwise it'll
have to wait until tomorrow.

Missed that promise by a day since something unexpected came up
yesterday.  Attached is v3 of the patch.  The changes are:

    - Rebased on top of the current master
    - Added a function pgp_armor_header_keys() to list all keys present
in an armor
    - Changed pgp_armor_header() to use a StringInfo instead of an mbuf
    - Fixed the "error is returned" problem in the documentation pointed
out earlier

Thanks! I found the pgp_extract_armor_headers()'s signature quite weird, so I simplified that by making it always return arrays of keys and values. The callers is now responsible for returning all the keys (pgp_armor_header_keys) or finding the single key (pgp_armor_header). I also partially rewrote the implementation of pgp_extract_armor_headers(), making it more readable I hope.

If an armor header line ends in CR+LF, pgp_armor_header() returned the CR as part of the value, with your patch. I don't think that's right, the line ending should be considered part of the armoring, so I changed that.

Is there any real life examples or tools out there to generate armors with headers with duplicate keys? RFC 4880 says:

   Note that some transport methods are sensitive to line length.  While
   there is a limit of 76 characters for the Radix-64 data (Section
   6.3), there is no limit to the length of Armor Headers.  Care should
   be taken that the Armor Headers are short enough to survive
   transport.  One way to do this is to repeat an Armor Header key
   multiple times with different values for each so that no one line is
   overly long.

Does anyone do that in practice? Is there any precedence for concatenating the values in other tools that read armor headers?

I wonder if it would make sense to have pgp_armor_header_keys() return both the keys and values. That would make it easier to use, and it might then make sense for it to not remove the duplicates or concatenate values, but just them as is. The caller could then deal with the duplicates any way he wants. We could keep the function for extracting the value for a single key, with the concatenating behavior, for convenience.

- Heikki

diff --git a/contrib/pgcrypto/Makefile b/contrib/pgcrypto/Makefile
index b6c9484..d1d75ca 100644
--- a/contrib/pgcrypto/Makefile
+++ b/contrib/pgcrypto/Makefile
@@ -26,7 +26,7 @@ MODULE_big	= pgcrypto
 OBJS		= $(SRCS:.c=.o) $(WIN32RES)
 
 EXTENSION = pgcrypto
-DATA = pgcrypto--1.1.sql pgcrypto--1.0--1.1.sql pgcrypto--unpackaged--1.0.sql
+DATA = pgcrypto--1.2.sql pgcrypto--1.1--1.2.sql pgcrypto--1.0--1.1.sql pgcrypto--unpackaged--1.0.sql
 PGFILEDESC = "pgcrypto - cryptographic functions"
 
 REGRESS = init md5 sha1 hmac-md5 hmac-sha1 blowfish rijndael \
diff --git a/contrib/pgcrypto/expected/pgp-armor.out b/contrib/pgcrypto/expected/pgp-armor.out
index c955494..9aa0b4b 100644
--- a/contrib/pgcrypto/expected/pgp-armor.out
+++ b/contrib/pgcrypto/expected/pgp-armor.out
@@ -102,3 +102,348 @@ em9va2E=
 -----END PGP MESSAGE-----
 ');
 ERROR:  Corrupt ascii-armor
+-- corrupt
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+foo:
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+ERROR:  Corrupt ascii-armor
+-- empty
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+foo: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+ pgp_armor_header 
+------------------
+ 
+(1 row)
+
+-- simple
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+foo: bar
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+ pgp_armor_header 
+------------------
+ bar
+(1 row)
+
+-- uninteresting keys, part 1
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+foo: found
+bar: ignored
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+ pgp_armor_header 
+------------------
+ found
+(1 row)
+
+-- uninteresting keys, part 2
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+bar: ignored
+foo: found
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+ pgp_armor_header 
+------------------
+ found
+(1 row)
+
+-- uninteresting keys, part 3
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+bar: ignored
+foo: found
+bar: ignored
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+ pgp_armor_header 
+------------------
+ found
+(1 row)
+
+-- insane keys, part 1
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+insane:key : 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'insane:key ');
+ pgp_armor_header 
+------------------
+ 
+(1 row)
+
+-- insane keys, part 2
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+insane:key : text value here
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'insane:key ');
+ pgp_armor_header 
+------------------
+ text value here
+(1 row)
+
+-- long value
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 76 characters long, but it should still parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'long');
+                                                pgp_armor_header                                                 
+-----------------------------------------------------------------------------------------------------------------
+ this value is more than 76 characters long, but it should still parse correctly as that's permitted by RFC 4880
+(1 row)
+
+-- long value, split up
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 76 characters long, but it should still 
+long: parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'long');
+                                                pgp_armor_header                                                 
+-----------------------------------------------------------------------------------------------------------------
+ this value is more than 76 characters long, but it should still parse correctly as that's permitted by RFC 4880
+(1 row)
+
+-- long value, split up, part 2
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 
+long: 76 characters long, but it should still 
+long: parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'long');
+                                                pgp_armor_header                                                 
+-----------------------------------------------------------------------------------------------------------------
+ this value is more than 76 characters long, but it should still parse correctly as that's permitted by RFC 4880
+(1 row)
+
+-- long value, split up, part 3
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+ignored: 
+long: this value is more than 
+ignored: 
+long: 76 characters long, but it should still 
+ignored: 
+long: parse correctly as that''s permitted by RFC 4880
+ignored: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'long');
+                                                pgp_armor_header                                                 
+-----------------------------------------------------------------------------------------------------------------
+ this value is more than 76 characters long, but it should still parse correctly as that's permitted by RFC 4880
+(1 row)
+
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+Comment: dat1.blowfish.sha1.mdc.s2k3.z0
+
+jA0EBAMCfFNwxnvodX9g0jwB4n4s26/g5VmKzVab1bX1SmwY7gvgvlWdF3jKisvS
+yA6Ce1QTMK3KdL2MPfamsTUSAML8huCJMwYQFfE=
+=JcP+
+-----END PGP MESSAGE-----
+', 'Comment');
+        pgp_armor_header        
+--------------------------------
+ dat1.blowfish.sha1.mdc.s2k3.z0
+(1 row)
+
+-- corrupt
+select pgp_armor_header_keys('
+-----BEGIN PGP MESSAGE-----
+foo:
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ERROR:  Corrupt ascii-armor
+-- empty
+select pgp_armor_header_keys('
+-----BEGIN PGP MESSAGE-----
+foo: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ pgp_armor_header_keys 
+-----------------------
+ foo
+(1 row)
+
+-- simple
+select pgp_armor_header_keys('
+-----BEGIN PGP MESSAGE-----
+foo: bar
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ pgp_armor_header_keys 
+-----------------------
+ foo
+(1 row)
+
+-- duplicates should be eliminated
+select pgp_armor_header_keys('
+-----BEGIN PGP MESSAGE-----
+nodups: 
+long: this value is more than 
+nodups: 
+long: 76 characters long, but it should still 
+nodups: 
+long: parse correctly as that''s permitted by RFC 4880
+nodups: 
+reallynodups: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+ pgp_armor_header_keys 
+-----------------------
+ nodups
+ long
+ reallynodups
+(3 rows)
+
+-- test different line endings
+with testmsg as
+(
+select '
+-----BEGIN PGP MESSAGE-----
+fookey: foovalue
+barkey: barvalue
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+'::text as msg)
+select pgp_armor_header_keys(msg) as lf_keys,
+       pgp_armor_header_keys(replace(msg, E'\n', E'\r\n')) as crlf_keys,
+       pgp_armor_header(msg, 'fookey') as lf_foovalue,
+       pgp_armor_header(replace(msg, E'\n', E'\r\n'), 'fookey') as crlf_foovalue,
+       pgp_armor_header(msg, 'barkey') as lf_barvalue,
+       pgp_armor_header(replace(msg, E'\n', E'\r\n'), 'barkey') as crlf_barvalue
+from testmsg
+;
+ lf_keys | crlf_keys | lf_foovalue | crlf_foovalue | lf_barvalue | crlf_barvalue 
+---------+-----------+-------------+---------------+-------------+---------------
+ fookey  | fookey    | foovalue    | foovalue      | barvalue    | barvalue
+ barkey  | barkey    | foovalue    | foovalue      | barvalue    | barvalue
+(2 rows)
+
+-- test header generation
+select armor('zooka', array['foo'], array['bar']);
+            armor            
+-----------------------------
+ -----BEGIN PGP MESSAGE-----+
+ foo: bar                   +
+                            +
+ em9va2E=                   +
+ =D5cR                      +
+ -----END PGP MESSAGE-----  +
+ 
+(1 row)
+
+select armor('zooka', array['Version', 'Comment'], array['Created by pgcrypto', 'PostgreSQL, the world''s most most advanced open source database']);
+                                  armor                                   
+--------------------------------------------------------------------------
+ -----BEGIN PGP MESSAGE-----                                             +
+ Version: Created by pgcrypto                                            +
+ Comment: PostgreSQL, the world's most most advanced open source database+
+                                                                         +
+ em9va2E=                                                                +
+ =D5cR                                                                   +
+ -----END PGP MESSAGE-----                                               +
+ 
+(1 row)
+
+select pgp_armor_header(armor('zooka', array['Version', 'Comment'], array['Created by pgcrypto', 'PostgreSQL, the world''s most most advanced open source database']), 'Comment');
+                        pgp_armor_header                         
+-----------------------------------------------------------------
+ PostgreSQL, the world's most most advanced open source database
+(1 row)
+
+-- error/corner cases
+select armor('', array['foo'], array['too', 'many']);
+ERROR:  mismatched array dimensions
+select armor('', array['too', 'many'], array['foo']);
+ERROR:  mismatched array dimensions
+select armor('', array[['']], array['foo']);
+ERROR:  wrong number of array subscripts
+select armor('', array['foo'], array[['']]);
+ERROR:  wrong number of array subscripts
+select armor('', array[null], array['foo']);
+ERROR:  null value not allowed for header key
+select armor('', array['foo'], array[null]);
+ERROR:  null value not allowed for header value
+select armor('', '[0:0]={"foo"}', array['foo']);
+            armor            
+-----------------------------
+ -----BEGIN PGP MESSAGE-----+
+ foo: foo                   +
+                            +
+ =twTO                      +
+ -----END PGP MESSAGE-----  +
+ 
+(1 row)
+
+select armor('', array['foo'], '[0:0]={"foo"}');
+            armor            
+-----------------------------
+ -----BEGIN PGP MESSAGE-----+
+ foo: foo                   +
+                            +
+ =twTO                      +
+ -----END PGP MESSAGE-----  +
+ 
+(1 row)
+
diff --git a/contrib/pgcrypto/pgcrypto--1.1--1.2.sql b/contrib/pgcrypto/pgcrypto--1.1--1.2.sql
new file mode 100644
index 0000000..8d3a385
--- /dev/null
+++ b/contrib/pgcrypto/pgcrypto--1.1--1.2.sql
@@ -0,0 +1,19 @@
+/* contrib/pgcrypto/pgcrypto--1.1--1.2.sql */
+
+-- complain if script is sourced in psql, rather than via ALTER EXTENSION
+\echo Use "ALTER EXTENSION pgcrypto UPDATE TO '1.2'" to load this file. \quit
+
+CREATE FUNCTION armor(bytea, text[], text[])
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_armor'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_armor_header(text, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_armor_header'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_armor_header_keys(text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_armor_header_keys'
+LANGUAGE C IMMUTABLE STRICT;
diff --git a/contrib/pgcrypto/pgcrypto--1.2.sql b/contrib/pgcrypto/pgcrypto--1.2.sql
new file mode 100644
index 0000000..5b8aafd
--- /dev/null
+++ b/contrib/pgcrypto/pgcrypto--1.2.sql
@@ -0,0 +1,222 @@
+/* contrib/pgcrypto/pgcrypto--1.1.sql */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION pgcrypto" to load this file. \quit
+
+CREATE FUNCTION digest(text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_digest'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION digest(bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_digest'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION hmac(text, text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_hmac'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION hmac(bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_hmac'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION crypt(text, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_crypt'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION gen_salt(text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_gen_salt'
+LANGUAGE C VOLATILE STRICT;
+
+CREATE FUNCTION gen_salt(text, int4)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_gen_salt_rounds'
+LANGUAGE C VOLATILE STRICT;
+
+CREATE FUNCTION encrypt(bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_encrypt'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION decrypt(bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_decrypt'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION encrypt_iv(bytea, bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_encrypt_iv'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION decrypt_iv(bytea, bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_decrypt_iv'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION gen_random_bytes(int4)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_random_bytes'
+LANGUAGE C VOLATILE STRICT;
+
+CREATE FUNCTION gen_random_uuid()
+RETURNS uuid
+AS 'MODULE_PATHNAME', 'pg_random_uuid'
+LANGUAGE C VOLATILE;
+
+--
+-- pgp_sym_encrypt(data, key)
+--
+CREATE FUNCTION pgp_sym_encrypt(text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_text'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pgp_sym_encrypt_bytea(bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_bytea'
+LANGUAGE C STRICT;
+
+--
+-- pgp_sym_encrypt(data, key, args)
+--
+CREATE FUNCTION pgp_sym_encrypt(text, text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_text'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pgp_sym_encrypt_bytea(bytea, text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_encrypt_bytea'
+LANGUAGE C STRICT;
+
+--
+-- pgp_sym_decrypt(data, key)
+--
+CREATE FUNCTION pgp_sym_decrypt(bytea, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_text'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_sym_decrypt_bytea(bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_bytea'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- pgp_sym_decrypt(data, key, args)
+--
+CREATE FUNCTION pgp_sym_decrypt(bytea, text, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_text'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_sym_decrypt_bytea(bytea, text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_sym_decrypt_bytea'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- pgp_pub_encrypt(data, key)
+--
+CREATE FUNCTION pgp_pub_encrypt(text, bytea)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_text'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pgp_pub_encrypt_bytea(bytea, bytea)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_bytea'
+LANGUAGE C STRICT;
+
+--
+-- pgp_pub_encrypt(data, key, args)
+--
+CREATE FUNCTION pgp_pub_encrypt(text, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_text'
+LANGUAGE C STRICT;
+
+CREATE FUNCTION pgp_pub_encrypt_bytea(bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_encrypt_bytea'
+LANGUAGE C STRICT;
+
+--
+-- pgp_pub_decrypt(data, key)
+--
+CREATE FUNCTION pgp_pub_decrypt(bytea, bytea)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_text'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_pub_decrypt_bytea(bytea, bytea)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_bytea'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- pgp_pub_decrypt(data, key, psw)
+--
+CREATE FUNCTION pgp_pub_decrypt(bytea, bytea, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_text'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_pub_decrypt_bytea(bytea, bytea, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_bytea'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- pgp_pub_decrypt(data, key, psw, arg)
+--
+CREATE FUNCTION pgp_pub_decrypt(bytea, bytea, text, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_text'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_pub_decrypt_bytea(bytea, bytea, text, text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pgp_pub_decrypt_bytea'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- PGP key ID
+--
+CREATE FUNCTION pgp_key_id(bytea)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_key_id_w'
+LANGUAGE C IMMUTABLE STRICT;
+
+--
+-- pgp armor
+--
+CREATE FUNCTION armor(bytea)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_armor'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION armor(bytea, text[], text[])
+RETURNS text
+AS 'MODULE_PATHNAME', 'pg_armor'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION dearmor(text)
+RETURNS bytea
+AS 'MODULE_PATHNAME', 'pg_dearmor'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_armor_header(text, text)
+RETURNS text
+AS 'MODULE_PATHNAME', 'pgp_armor_header'
+LANGUAGE C IMMUTABLE STRICT;
+
+CREATE FUNCTION pgp_armor_header_keys(text)
+RETURNS SETOF text
+AS 'MODULE_PATHNAME', 'pgp_armor_header_keys'
+LANGUAGE C IMMUTABLE STRICT;
diff --git a/contrib/pgcrypto/pgcrypto.control b/contrib/pgcrypto/pgcrypto.control
index 7f79d04..bb6885b 100644
--- a/contrib/pgcrypto/pgcrypto.control
+++ b/contrib/pgcrypto/pgcrypto.control
@@ -1,5 +1,5 @@
 # pgcrypto extension
 comment = 'cryptographic functions'
-default_version = '1.1'
+default_version = '1.2'
 module_pathname = '$libdir/pgcrypto'
 relocatable = true
diff --git a/contrib/pgcrypto/pgp-armor.c b/contrib/pgcrypto/pgp-armor.c
index ec647f0..5bd02e7 100644
--- a/contrib/pgcrypto/pgp-armor.c
+++ b/contrib/pgcrypto/pgp-armor.c
@@ -178,7 +178,7 @@ b64_dec_len(unsigned srclen)
  * PGP armor
  */
 
-static const char *armor_header = "-----BEGIN PGP MESSAGE-----\n\n";
+static const char *armor_header = "-----BEGIN PGP MESSAGE-----\n";
 static const char *armor_footer = "\n-----END PGP MESSAGE-----\n";
 
 /* CRC24 implementation from rfc2440 */
@@ -204,17 +204,24 @@ crc24(const uint8 *data, unsigned len)
 }
 
 void
-pgp_armor_encode(const uint8 *src, int len, StringInfo dst)
+pgp_armor_encode(const uint8 *src, unsigned len, StringInfo dst,
+				 int num_headers, char **keys, char **values)
 {
+	int			n;
 	int			res;
 	unsigned	b64len;
 	unsigned	crc = crc24(src, len);
 
 	appendStringInfoString(dst, armor_header);
 
+	for (n = 0; n < num_headers; n++)
+		appendStringInfo(dst, "%s: %s\n", keys[n], values[n]);
+	appendStringInfoChar(dst, '\n');
+
 	/* make sure we have enough room to b64_encode() */
 	b64len = b64_enc_len(len);
 	enlargeStringInfo(dst, (int) b64len);
+
 	res = b64_encode(src, len, (uint8 *) dst->data + dst->len);
 	if (res > b64len)
 		elog(FATAL, "overflow - encode estimate too small");
@@ -371,3 +378,108 @@ pgp_armor_decode(const uint8 *src, int len, StringInfo dst)
 out:
 	return res;
 }
+
+/*
+ * Extracts all armor headers from an ASCII-armored input.
+ *
+ * Returns 0 on success, or PXE_* error code on error. On success, the
+ * number of headers and their keys and values are returned in *nheaders,
+ * *nkeys and *nvalues.
+ */
+int
+pgp_extract_armor_headers(const uint8 *src, unsigned len,
+						  int *nheaders, char ***keys, char ***values)
+{
+	const uint8 *data_end = src + len;
+	const uint8 *p;
+	const uint8 *base64_start;
+	const uint8 *armor_start;
+	const uint8 *armor_end;
+	Size		armor_len;
+	char	   *line;
+	char	   *nextline;
+	char	   *eol,
+				*colon;
+	int			hlen;
+	char	   *buf;
+	int			hdrlines;
+	int			n;
+
+	/* armor start */
+	hlen = find_header(src, data_end, &armor_start, 0);
+	if (hlen <= 0)
+		return PXE_PGP_CORRUPT_ARMOR;
+	armor_start += hlen;
+
+	/* armor end */
+	hlen = find_header(armor_start, data_end, &armor_end, 1);
+	if (hlen <= 0)
+		return PXE_PGP_CORRUPT_ARMOR;
+
+	/* Count the number of armor header lines. */
+	hdrlines = 0;
+	p = armor_start;
+	while (p < armor_end && *p != '\n' && *p != '\r')
+	{
+		p = memchr(p, '\n', armor_end - p);
+		if (!p)
+			return PXE_PGP_CORRUPT_ARMOR;
+
+		/* step to start of next line */
+		p++;
+		hdrlines++;
+	}
+	base64_start = p;
+
+	/*
+	 * Make a modifiable copy of the part of the input that contains the
+	 * headers. The returned key/value pointers will point inside the buffer.
+	 */
+	armor_len = base64_start - armor_start;
+	buf = palloc(armor_len + 1);
+	memcpy(buf, armor_start, armor_len);
+	buf[armor_len] = '\0';
+
+	/* Allocate return arrays */
+	*keys = (char **) palloc(hdrlines * sizeof(char *));
+	*values = (char **) palloc(hdrlines * sizeof(char *));
+
+	/* Read all the header lines into the arrays */
+	n = 0;
+	line = buf;
+	for (;;)
+	{
+		/* find end of line */
+		eol = strchr(line, '\n');
+		if (!eol)
+			break;
+		nextline = eol + 1;
+		/* if the line ends in CR + LF, strip the CR */
+		if (eol > line && *(eol - 1) == '\r')
+			eol--;
+		*eol = '\0';
+
+		/* find colon+space separating the key and value */
+		colon = strstr(line, ": ");
+		if (!colon)
+			return PXE_PGP_CORRUPT_ARMOR;
+		*colon = '\0';
+
+		/* shouldn't happen */
+		if (n >= hdrlines)
+			elog(ERROR, "unexpected number of armor header lines");
+
+		(*keys)[n] = line;
+		(*values)[n] = colon + 2;
+		n++;
+
+		/* step to start of next line */
+		line = nextline;
+	}
+
+	if (n != hdrlines)
+		elog(ERROR, "unexpected number of armor header lines");
+
+	*nheaders = n;
+	return 0;
+}
diff --git a/contrib/pgcrypto/pgp-pgsql.c b/contrib/pgcrypto/pgp-pgsql.c
index 5d2d465..71fd44d 100644
--- a/contrib/pgcrypto/pgp-pgsql.c
+++ b/contrib/pgcrypto/pgp-pgsql.c
@@ -32,8 +32,11 @@
 #include "postgres.h"
 
 #include "lib/stringinfo.h"
+#include "catalog/pg_type.h"
 #include "mb/pg_wchar.h"
 #include "utils/builtins.h"
+#include "utils/array.h"
+#include "funcapi.h"
 
 #include "mbuf.h"
 #include "px.h"
@@ -56,6 +59,8 @@ PG_FUNCTION_INFO_V1(pgp_key_id_w);
 
 PG_FUNCTION_INFO_V1(pg_armor);
 PG_FUNCTION_INFO_V1(pg_dearmor);
+PG_FUNCTION_INFO_V1(pgp_armor_header);
+PG_FUNCTION_INFO_V1(pgp_armor_header_keys);
 
 /*
  * Mix a block of data into RNG.
@@ -816,6 +821,73 @@ pgp_pub_decrypt_text(PG_FUNCTION_ARGS)
  * Wrappers for PGP ascii armor
  */
 
+/*
+ * Helper function for pgp_armor. Converts arrays of keys and values into
+ * plain C arrays containing UTF-8 strings.
+ */
+static int
+parse_key_value_arrays(ArrayType *key_array, ArrayType *val_array,
+					   char ***p_keys, char ***p_values)
+{
+	int		nkdims = ARR_NDIM(key_array);
+	int		nvdims = ARR_NDIM(val_array);
+	char   **keys,
+		   **values;
+	Datum  *key_datums,
+		   *val_datums;
+	bool   *key_nulls,
+		   *val_nulls;
+	int		key_count,
+			val_count;
+	int		i;
+
+	if (nkdims > 1 || nkdims != nvdims)
+		ereport(ERROR,
+				(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+				errmsg("wrong number of array subscripts")));
+	if (nkdims == 0)
+		return 0;
+
+	deconstruct_array(key_array,
+					  TEXTOID, -1, false, 'i',
+					  &key_datums, &key_nulls, &key_count);
+
+	deconstruct_array(val_array,
+					  TEXTOID, -1, false, 'i',
+					  &val_datums, &val_nulls, &val_count);
+
+	if (key_count != val_count)
+		ereport(ERROR,
+				(errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
+				 errmsg("mismatched array dimensions")));
+
+	keys = (char **) palloc(sizeof(char *) * key_count);
+	values = (char **) palloc(sizeof(char *) * val_count);
+
+	for (i = 0; i < key_count; i++)
+	{
+		char *v;
+
+		if (key_nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					 errmsg("null value not allowed for header key")));
+		if (val_nulls[i])
+			ereport(ERROR,
+					(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
+					 errmsg("null value not allowed for header value")));
+
+		v = TextDatumGetCString(key_datums[i]);
+		keys[i] = pg_server_to_any(v, strlen(v), PG_UTF8);
+		v = TextDatumGetCString(val_datums[i]);
+		values[i] = pg_server_to_any(v, strlen(v), PG_UTF8);
+	}
+
+	*p_keys = keys;
+	*p_values = values;
+	return key_count;
+}
+
 Datum
 pg_armor(PG_FUNCTION_ARGS)
 {
@@ -823,13 +895,31 @@ pg_armor(PG_FUNCTION_ARGS)
 	text	   *res;
 	int			data_len;
 	StringInfoData buf;
+	int			num_headers;
+	char	  **keys,
+			  **values;
 
 	data = PG_GETARG_BYTEA_P(0);
 	data_len = VARSIZE(data) - VARHDRSZ;
+	if (PG_NARGS() == 3)
+	{
+		num_headers = parse_key_value_arrays(PG_GETARG_ARRAYTYPE_P(1),
+											 PG_GETARG_ARRAYTYPE_P(2),
+											 &keys, &values);
+	}
+	else if (PG_NARGS() == 1)
+	{
+		num_headers = 0;
+		keys = NULL;
+		values = NULL;
+	}
+	else
+		elog(ERROR, "unexpected number of arguments %d", PG_NARGS());
 
 	initStringInfo(&buf);
 
-	pgp_armor_encode((uint8 *) VARDATA(data), data_len, &buf);
+	pgp_armor_encode((uint8 *) VARDATA(data), data_len, &buf,
+					 num_headers, keys, values);
 
 	res = palloc(VARHDRSZ + buf.len);
 	SET_VARSIZE(res, VARHDRSZ + buf.len);
@@ -868,6 +958,126 @@ pg_dearmor(PG_FUNCTION_ARGS)
 	PG_RETURN_TEXT_P(res);
 }
 
+Datum
+pgp_armor_header(PG_FUNCTION_ARGS)
+{
+	bytea	   *data = PG_GETARG_BYTEA_PP(0);
+	char 	   *key = TextDatumGetCString(PG_GETARG_TEXT_PP(1));
+	int			res;
+	char	   *utf8key;
+	StringInfoData buf;
+	int			nheaders;
+	char	  **keys;
+	char	  **values;
+	int			i;
+
+	res = pgp_extract_armor_headers((uint8 *) VARDATA_ANY(data),
+									VARSIZE_ANY_EXHDR(data),
+									&nheaders, &keys, &values);
+	if (res < 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_EXTERNAL_ROUTINE_INVOCATION_EXCEPTION),
+				 errmsg("%s", px_strerror(res))));
+
+	/*
+	 * Find the header we were asked for. If there are multiple headers
+	 * with the same key, concatenate the values.
+	 */
+	utf8key = pg_server_to_any(key, strlen(key), PG_UTF8);
+
+	initStringInfo(&buf);
+
+	for (i = 0; i < nheaders; i++)
+	{
+		if (strcmp(keys[i], utf8key) == 0)
+			appendStringInfoString(&buf, values[i]);
+	}
+
+	if (buf.len > 0)
+	{
+		/* assume all the headers to be in UTF-8 */
+		char *utf = pg_any_to_server(buf.data, buf.len - 1, PG_UTF8);
+		PG_RETURN_TEXT_P(cstring_to_text(utf));
+	}
+	PG_RETURN_NULL();
+}
+
+/* cross-call state for pgp_armor_header_keys */
+typedef struct
+{
+	int			nheaders;
+	char	  **keys;
+	char	  **values;
+	int			currkey;
+} pgp_armor_header_keys_state;
+
+Datum
+pgp_armor_header_keys(PG_FUNCTION_ARGS)
+{
+	FuncCallContext *funcctx;
+	pgp_armor_header_keys_state *state;
+	int			i;
+	char	   *utf8key;
+	text	   *result;
+
+	if (SRF_IS_FIRSTCALL())
+	{
+		bytea	   *data = PG_GETARG_BYTEA_PP(0);
+		int			res;
+		MemoryContext oldcontext;
+
+		funcctx = SRF_FIRSTCALL_INIT();
+
+		/* we need the state allocated in the multi call context */
+		oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
+		state = (pgp_armor_header_keys_state *) palloc(sizeof(pgp_armor_header_keys_state));
+
+		res = pgp_extract_armor_headers((uint8 *) VARDATA_ANY(data),
+										VARSIZE_ANY_EXHDR(data),
+										&state->nheaders, &state->keys,
+										&state->values);
+		if (res < 0)
+			ereport(ERROR,
+					(errcode(ERRCODE_EXTERNAL_ROUTINE_INVOCATION_EXCEPTION),
+					 errmsg("%s", px_strerror(res))));
+
+		MemoryContextSwitchTo(oldcontext);
+		funcctx->user_fctx = state;
+		state->currkey = 0;
+	}
+
+	funcctx = SRF_PERCALL_SETUP();
+	state = (pgp_armor_header_keys_state *) funcctx->user_fctx;
+
+nextkey:
+	if (state->currkey >= state->nheaders)
+		SRF_RETURN_DONE(funcctx);
+	else
+	{
+		/*
+		 * We don't want duplicate keys in the output, so check if we returned
+		 * this key already.
+		 */
+		for (i = 0; i < state->currkey; i++)
+		{
+			if (strcmp(state->keys[state->currkey], state->keys[i]) == 0)
+			{
+				state->currkey++;
+				goto nextkey;
+			}
+		}
+
+		/* we assume that the keys (and values) are in UTF-8. */
+		utf8key = state->keys[state->currkey];
+		result = cstring_to_text(pg_any_to_server(utf8key, strlen(utf8key), PG_UTF8));
+
+		state->currkey++;
+		SRF_RETURN_NEXT(funcctx, PointerGetDatum(result));
+	}
+}
+
+
+
 /*
  * Wrappers for PGP key id
  */
diff --git a/contrib/pgcrypto/pgp.h b/contrib/pgcrypto/pgp.h
index cecd181..398f21b 100644
--- a/contrib/pgcrypto/pgp.h
+++ b/contrib/pgcrypto/pgp.h
@@ -276,8 +276,11 @@ void		pgp_cfb_free(PGP_CFB *ctx);
 int			pgp_cfb_encrypt(PGP_CFB *ctx, const uint8 *data, int len, uint8 *dst);
 int			pgp_cfb_decrypt(PGP_CFB *ctx, const uint8 *data, int len, uint8 *dst);
 
-void		pgp_armor_encode(const uint8 *src, int len, StringInfo dst);
+void		pgp_armor_encode(const uint8 *src, unsigned len, StringInfo dst,
+							 int num_headers, char **keys, char **values);
 int			pgp_armor_decode(const uint8 *src, int len, StringInfo dst);
+int			pgp_extract_armor_headers(const uint8 *src, unsigned len,
+									  int *nheaders, char ***keys, char ***values);
 
 int			pgp_compress_filter(PushFilter **res, PGP_Context *ctx, PushFilter *dst);
 int			pgp_decompress_filter(PullFilter **res, PGP_Context *ctx, PullFilter *src);
diff --git a/contrib/pgcrypto/sql/pgp-armor.sql b/contrib/pgcrypto/sql/pgp-armor.sql
index 71ffba2..64acffa 100644
--- a/contrib/pgcrypto/sql/pgp-armor.sql
+++ b/contrib/pgcrypto/sql/pgp-armor.sql
@@ -56,3 +56,229 @@ em9va2E=
 =ZZZZ
 -----END PGP MESSAGE-----
 ');
+
+-- corrupt
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+foo:
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+
+-- empty
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+foo: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+
+-- simple
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+foo: bar
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+
+-- uninteresting keys, part 1
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+foo: found
+bar: ignored
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+
+-- uninteresting keys, part 2
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+bar: ignored
+foo: found
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+
+-- uninteresting keys, part 3
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+bar: ignored
+foo: found
+bar: ignored
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'foo');
+
+-- insane keys, part 1
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+insane:key : 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'insane:key ');
+
+-- insane keys, part 2
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+insane:key : text value here
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'insane:key ');
+
+-- long value
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 76 characters long, but it should still parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'long');
+
+-- long value, split up
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 76 characters long, but it should still 
+long: parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'long');
+
+-- long value, split up, part 2
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+long: this value is more than 
+long: 76 characters long, but it should still 
+long: parse correctly as that''s permitted by RFC 4880
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'long');
+
+-- long value, split up, part 3
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+ignored: 
+long: this value is more than 
+ignored: 
+long: 76 characters long, but it should still 
+ignored: 
+long: parse correctly as that''s permitted by RFC 4880
+ignored: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+', 'long');
+
+select pgp_armor_header('
+-----BEGIN PGP MESSAGE-----
+Comment: dat1.blowfish.sha1.mdc.s2k3.z0
+
+jA0EBAMCfFNwxnvodX9g0jwB4n4s26/g5VmKzVab1bX1SmwY7gvgvlWdF3jKisvS
+yA6Ce1QTMK3KdL2MPfamsTUSAML8huCJMwYQFfE=
+=JcP+
+-----END PGP MESSAGE-----
+', 'Comment');
+
+-- corrupt
+select pgp_armor_header_keys('
+-----BEGIN PGP MESSAGE-----
+foo:
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- empty
+select pgp_armor_header_keys('
+-----BEGIN PGP MESSAGE-----
+foo: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- simple
+select pgp_armor_header_keys('
+-----BEGIN PGP MESSAGE-----
+foo: bar
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- duplicates should be eliminated
+select pgp_armor_header_keys('
+-----BEGIN PGP MESSAGE-----
+nodups: 
+long: this value is more than 
+nodups: 
+long: 76 characters long, but it should still 
+nodups: 
+long: parse correctly as that''s permitted by RFC 4880
+nodups: 
+reallynodups: 
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+');
+
+-- test different line endings
+with testmsg as
+(
+select '
+-----BEGIN PGP MESSAGE-----
+fookey: foovalue
+barkey: barvalue
+
+em9va2E=
+=ZZZZ
+-----END PGP MESSAGE-----
+'::text as msg)
+select pgp_armor_header_keys(msg) as lf_keys,
+       pgp_armor_header_keys(replace(msg, E'\n', E'\r\n')) as crlf_keys,
+       pgp_armor_header(msg, 'fookey') as lf_foovalue,
+       pgp_armor_header(replace(msg, E'\n', E'\r\n'), 'fookey') as crlf_foovalue,
+       pgp_armor_header(msg, 'barkey') as lf_barvalue,
+       pgp_armor_header(replace(msg, E'\n', E'\r\n'), 'barkey') as crlf_barvalue
+from testmsg
+;
+
+-- test header generation
+select armor('zooka', array['foo'], array['bar']);
+select armor('zooka', array['Version', 'Comment'], array['Created by pgcrypto', 'PostgreSQL, the world''s most most advanced open source database']);
+select pgp_armor_header(armor('zooka', array['Version', 'Comment'], array['Created by pgcrypto', 'PostgreSQL, the world''s most most advanced open source database']), 'Comment');
+
+-- error/corner cases
+select armor('', array['foo'], array['too', 'many']);
+select armor('', array['too', 'many'], array['foo']);
+select armor('', array[['']], array['foo']);
+select armor('', array['foo'], array[['']]);
+select armor('', array[null], array['foo']);
+select armor('', array['foo'], array[null]);
+select armor('', '[0:0]={"foo"}', array['foo']);
+select armor('', array['foo'], '[0:0]={"foo"}');
diff --git a/doc/src/sgml/pgcrypto.sgml b/doc/src/sgml/pgcrypto.sgml
index 544a1f8..76f453a 100644
--- a/doc/src/sgml/pgcrypto.sgml
+++ b/doc/src/sgml/pgcrypto.sgml
@@ -691,13 +691,61 @@ pgp_key_id(bytea) returns text
    </indexterm>
 
 <synopsis>
-armor(data bytea) returns text
+armor(data bytea [ , keys text[], values text[] ]) returns text
 dearmor(data text) returns bytea
 </synopsis>
    <para>
     These functions wrap/unwrap binary data into PGP ASCII-armor format,
     which is basically Base64 with CRC and additional formatting.
    </para>
+
+   <para>
+    For <function>armor</>, if the <parameter>keys</> and <parameter>values</>
+    arrays are specified, their members are written into the armored data as
+    <literal>armor headers</>.  For each member in <parameter>keys</>, the
+    value in <parameter>values</> with the corresponding ordinal is used as
+    the value for that key.  Both arrays must be single-dimensional, and they
+    must be of the same length.  All text is converted into UTF-8.
+   </para>
+  </sect3>
+
+  <sect3>
+   <title><function>pgp_armor_header</function></title>
+
+   <indexterm>
+    <primary>pgp_armor_header</primary>
+   </indexterm>
+
+<synopsis>
+pgp_armor_header(data text, key text) returns text
+</synopsis>
+   <para>
+    <function>pgp_armor_header()</> extracts the <literal>armor header</> with
+    the key <parameter>key</> from <parameter>data</>.  Before matching,
+    <parameter>key</> is converted into UTF-8.  Also all data in the armored
+    text is assumed to be UTF-8.  If part of the data is not valid UTF-8 or
+    <parameter>key</> can not be converted to UTF-8, an error is raised.
+    If the key <parameter>key</> appears multiple times in the armored text,
+    all values are concatenated into the return value.  If the key does not
+    appear in the armored text, the return value is NULL.
+   </para>
+  </sect3>
+
+  <sect3>
+   <title><function>pgp_armor_header_keys</function></title>
+
+   <indexterm>
+    <primary>pgp_armor_header_keys</primary>
+   </indexterm>
+
+<synopsis>
+pgp_armor_header_keys(data text) returns setof text
+</synopsis>
+   <para>
+    <function>pgp_armor_header_keys()</> extracts the list of <literal>armor
+    header</> keys from <parameter>data</>.  The keys are all assumed to be in
+    UTF-8.  If any of the keys is not valid UTF-8, an error is raised.
+   </para>
   </sect3>
 
   <sect3>
-- 
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers

Reply via email to