From 0b33d1a0e8379296856599fedc893d42121c199b Mon Sep 17 00:00:00 2001
From: Stephan Bosch <stephan.bosch@open-xchange.com>
Date: Sun, 10 Oct 2021 22:12:23 +0200
Subject: [PATCH] lib-managesieve: Implement Sieve URL API.

---
 src/lib-managesieve/Makefile.am            |  31 +-
 src/lib-managesieve/managesieve-url.c      | 509 +++++++++++++++++++++
 src/lib-managesieve/managesieve-url.h      |  89 ++++
 src/lib-managesieve/test-managesieve-url.c | 345 ++++++++++++++
 4 files changed, 972 insertions(+), 2 deletions(-)
 create mode 100644 src/lib-managesieve/managesieve-url.c
 create mode 100644 src/lib-managesieve/managesieve-url.h
 create mode 100644 src/lib-managesieve/test-managesieve-url.c

diff --git a/src/lib-managesieve/Makefile.am b/src/lib-managesieve/Makefile.am
index 079df4359..54558ea9b 100644
--- a/src/lib-managesieve/Makefile.am
+++ b/src/lib-managesieve/Makefile.am
@@ -7,10 +7,37 @@ AM_CPPFLAGS = \
 libmanagesieve_la_SOURCES = \
 	managesieve-arg.c \
 	managesieve-quote.c \
-	managesieve-parser.c
+	managesieve-parser.c \
+	managesieve-url.c
 
 noinst_HEADERS = \
 	managesieve-protocol.h \
 	managesieve-arg.h \
 	managesieve-quote.h \
-	managesieve-parser.h
+	managesieve-parser.h \
+	managesieve-url.h
+
+test_programs = \
+	test-managesieve-url
+
+test_nocheck_programs =
+
+noinst_PROGRAMS = $(test_programs) $(test_nocheck_programs)
+
+test_libs = \
+	$(noinst_LTLIBRARIES) \
+	$(LIBDOVECOT_STORAGE) \
+	$(LIBDOVECOT)
+test_deps = \
+	$(noinst_LTLIBRARIES) \
+	$(LIBDOVECOT_STORAGE_DEPS) \
+	$(LIBDOVECOT_DEPS)
+
+test_managesieve_url_SOURCES = test-managesieve-url.c
+test_managesieve_url_LDADD = $(test_libs)
+test_managesieve_url_DEPENDENCIES = $(test_deps)
+
+check-local:
+	for bin in $(test_programs); do \
+	  if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+	done
diff --git a/src/lib-managesieve/managesieve-url.c b/src/lib-managesieve/managesieve-url.c
new file mode 100644
index 000000000..9d7cdb874
--- /dev/null
+++ b/src/lib-managesieve/managesieve-url.c
@@ -0,0 +1,509 @@
+/* Copyright (c) 2021 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "str.h"
+#include "strfuncs.h"
+#include "net.h"
+#include "uri-util.h"
+
+#include "managesieve-url.h"
+
+/* RFC 5804, Section 3:
+
+   sieveurl = sieveurl-server / sieveurl-list-scripts /
+              sieveurl-script
+
+   sieveurl-server = "sieve://" authority
+
+   sieveurl-list-scripts = "sieve://" authority ["/"]
+
+   sieveurl-script = "sieve://" authority "/"
+                     [owner "/"] scriptname
+
+   authority = <defined in [URI-GEN]>
+
+   owner         = *ochar
+                   ;; %-encoded version of [SASL] authorization
+                   ;; identity (script owner) or "userid".
+                   ;;
+                   ;; Empty owner is used to reference
+                   ;; global scripts.
+                   ;;
+                   ;; Note that ASCII characters such as " ", ";",
+                   ;; "&", "=", "/" and "?" must be %-encoded
+                   ;; as per rule specified in [URI-GEN].
+
+   scriptname    = 1*ochar
+                   ;; %-encoded version of UTF-8 representation
+                   ;; of the script name.
+                   ;; Note that ASCII characters such as " ", ";",
+                   ;; "&", "=", "/" and "?" must be %-encoded
+                   ;; as per rule specified in [URI-GEN].
+
+   ochar         = unreserved / pct-encoded / sub-delims-sh /
+                   ":" / "@"
+                   ;; Same as [URI-GEN] 'pchar',
+                   ;; but without ";", "&" and "=".
+
+   unreserved = <defined in [URI-GEN]>
+
+   pct-encoded = <defined in [URI-GEN]>
+
+   sub-delims-sh = "!" / "$" / "'" / "(" / ")" /
+                   "*" / "+" / ","
+                   ;; Same as [URI-GEN] sub-delims,
+                   ;; but without ";", "&" and "=".
+ */
+
+/* Character lookup table
+
+   unreserved    = ALPHA / DIGIT / "-" / "." / "_" / "~"     [bit0]
+   sub-delims-sh = "!" / "$" / "'" / "(" / ")" /
+                   "*" / "+" / ","
+                   ;; Same as [URI-GEN] sub-delims,
+                   ;; but without ";", "&" and "=".          [bit1]
+   ochar         = unreserved / pct-encoded / sub-delims-sh /
+                   ":" / "@"                                 [bit0|bit1|bit2]
+ */
+
+static const unsigned char managesieve_url_ochar_mask = (1<<0)|(1<<1)|(1<<2);
+
+static const unsigned char managesieve_url_char_lookup[256] = {
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  // 00
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  // 10
+	 2,  0,  0,  0,  2,  0,  0,  2,  2,  2,  2,  2,  2,  1,  1,  0,  // 20
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  4,  0,  0,  0,  0,  0,  // 30
+	 4,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  // 40
+	 1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  0,  0,  0,  0,  1,  // 50
+	 0,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  // 60
+	 1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  1,  0,  0,  0,  1,  0,  // 70
+
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  // 80
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  // 90
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  // a0
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  // b0
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  // c0
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  // d0
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  // e0
+	 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  // f0
+};
+
+/*
+ * Sieve URL parser
+ */
+
+struct managesieve_url_parser {
+	struct uri_parser parser;
+
+	enum managesieve_url_parse_flags flags;
+
+	struct managesieve_url *url;
+	struct managesieve_url *base;
+};
+
+static int
+managesieve_url_parse_scheme(struct managesieve_url_parser *url_parser)
+{
+	struct uri_parser *parser = &url_parser->parser;
+	const char *scheme;
+	int ret;
+
+	if ((url_parser->flags & MANAGESIEVE_URL_PARSE_SCHEME_EXTERNAL) != 0)
+		return 1;
+
+	ret = uri_parse_scheme(parser, &scheme);
+	if (ret < 0)
+		return -1;
+	if (ret == 0) {
+		parser->error = "Relative Sieve URL not allowed";
+		return -1;
+	}
+
+	if (strcasecmp(scheme, "sieve") != 0) {
+		parser->error = "Not a Sieve URL";
+		return -1;
+	}
+	return 0;
+}
+
+static int
+managesieve_url_parse_userinfo(struct managesieve_url_parser *url_parser,
+			       struct uri_authority *auth,
+			       const char **user_r, const char **password_r)
+{
+	struct uri_parser *parser = &url_parser->parser;
+	const char *p;
+
+	*user_r = *password_r = NULL;
+
+	if (auth->enc_userinfo == NULL)
+		return 0;
+	if ((url_parser->flags & MANAGESIEVE_URL_ALLOW_USERINFO_PART) == 0) {
+		parser->error = "Sieve URL does not allow `userinfo@' part";
+		return -1;
+	}
+
+	p = strchr(auth->enc_userinfo, ':');
+	if (p == NULL) {
+		if (!uri_data_decode(parser, auth->enc_userinfo, NULL, user_r))
+			return -1;
+	} else {
+		if (!uri_data_decode(parser, auth->enc_userinfo, p, user_r))
+			return -1;
+		if (!uri_data_decode(parser, p + 1, NULL, password_r))
+			return -1;
+	}
+	return 0;
+}
+
+static int
+managesieve_url_parse_authority(struct managesieve_url_parser *url_parser)
+{
+	struct uri_parser *parser = &url_parser->parser;
+	struct managesieve_url *url = url_parser->url;
+	struct uri_authority auth;
+	const char *user = NULL, *password = NULL;
+	int ret;
+
+	if ((ret = uri_parse_host_authority(parser, &auth)) < 0)
+		return -1;
+	if (auth.host.name == NULL || *auth.host.name == '\0') {
+		parser->error =
+			"Sieve URL does not allow empty host identifier";
+		return -1;
+	}
+	if (ret > 0) {
+		if (managesieve_url_parse_userinfo(url_parser, &auth,
+						   &user, &password) < 0)
+			return -1;
+	}
+	if (url != NULL) {
+		uri_host_copy(parser->pool, &url->host, &auth.host);
+		url->port = auth.port;
+		url->user = p_strdup(parser->pool, user);
+		url->password = p_strdup(parser->pool, password);
+	}
+	return 0;
+}
+
+static int
+managesieve_url_parse_path_segment(struct managesieve_url_parser *url_parser,
+				   const char **segment_r) ATTR_NULL(2)
+{
+	struct uri_parser *parser = &url_parser->parser;
+	const unsigned char *first, *offset;
+	string_t *segment = NULL;
+	int ret;
+
+	first = offset = parser->cur;
+	if (segment_r != NULL)
+		segment = t_str_new(128);
+	while (parser->cur < parser->end) {
+		if (*parser->cur == '%') {
+			unsigned char ch = 0;
+
+			if (segment != NULL) {
+				str_append_data(segment, offset,
+						parser->cur - offset);
+			}
+
+			ret = uri_parse_pct_encoded(parser, &ch);
+			if (ret < 0)
+				return -1;
+			i_assert(ret > 0);
+
+			if (segment != NULL)
+				str_append_c(segment, ch);
+			offset = parser->cur;
+			continue;
+		}
+		if ((managesieve_url_char_lookup[*parser->cur] &
+		     managesieve_url_ochar_mask) == 0)
+			break;
+		parser->cur++;
+	}
+	if (segment != NULL)
+		str_append_data(segment, offset, parser->cur - offset);
+
+	if (parser->cur < parser->end && *parser->cur != '/' &&
+	    *parser->cur != '?' && *parser->cur != '#') {
+		parser->error = p_strdup_printf(parser->pool,
+			"Path segment contains invalid character %s",
+			uri_char_sanitize(*parser->cur));
+		return -1;
+	}
+
+	if (first == parser->cur)
+		return 0;
+
+	if (segment != NULL)
+		*segment_r = p_strdup(parser->pool, str_c(segment));
+	return 1;
+}
+
+static int
+managesieve_url_parse_path(struct managesieve_url_parser *url_parser)
+{
+	struct uri_parser *parser = &url_parser->parser;
+	struct managesieve_url *url = url_parser->url;
+	const char *segment1, *segment2;
+	int ret;
+
+	if (parser->cur >= parser->end || *parser->cur != '/')
+		return 0;
+	parser->cur++;
+
+	ret = managesieve_url_parse_path_segment(url_parser,
+						 (url == NULL ?
+						  NULL : &segment1));
+	if (ret < 0)
+		return -1;
+	if (ret == 0) {
+		if (url != NULL)
+			url->scriptname = "";
+		return 1;
+	}
+
+	if (parser->cur >= parser->end || *parser->cur != '/') {
+		if (url != NULL)
+			url->scriptname = segment1;
+		return 1;
+	}
+	parser->cur++;
+
+	ret = managesieve_url_parse_path_segment(url_parser,
+						 (url == NULL ?
+						  NULL : &segment2));
+	if (ret < 0)
+		return -1;
+	if (ret == 0) {
+		parser->error = "Empty script name";
+		return -1;
+	}
+	if (*parser->cur == '/') {
+		parser->error = "Script name contains invalid character '/'";
+		return -1;
+	}
+
+	if (url != NULL) {
+		url->owner = segment1;
+		url->scriptname = segment2;
+	}
+	return 1;
+}
+
+static int managesieve_url_do_parse(struct managesieve_url_parser *url_parser)
+{
+	struct uri_parser *parser = &url_parser->parser;
+
+	/* "sieve:" */
+	if (managesieve_url_parse_scheme(url_parser) < 0)
+		return -1;
+
+	/* "//" authority
+	 */
+	if (parser->cur >= parser->end || parser->cur[0] != '/' ||
+	    (parser->cur + 1) >= parser->end || parser->cur[1] != '/') {
+		parser->error = "Sieve URL requires `//' after `sieve:'";
+		return -1;
+	}
+	parser->cur += 2;
+
+	if (managesieve_url_parse_authority(url_parser) < 0)
+		return -1;
+
+	/*  "/" [owner "/"] scriptname */
+	if (managesieve_url_parse_path(url_parser) < 0)
+		return -1;
+
+	/* Sieve URL has no query */
+	if (*parser->cur == '?') {
+		parser->error = "Query component not allowed in Sieve URL";
+		return -1;
+	}
+
+	/* Sieve URL has no fragment */
+	if (*parser->cur == '#') {
+		parser->error = "Fragment component not allowed in Sieve URL";
+		return -1;
+	}
+
+	/* Must be at end of URL now */
+	i_assert(parser->cur == parser->end);
+
+	return 0;
+}
+
+/* Public API */
+
+int managesieve_url_parse(const char *url,
+			  enum managesieve_url_parse_flags flags, pool_t pool,
+			  struct managesieve_url **url_r, const char **error_r)
+{
+	struct managesieve_url_parser url_parser;
+
+	i_zero(&url_parser);
+	uri_parser_init(&url_parser.parser, pool, url);
+
+	if (url_r != NULL)
+		url_parser.url = p_new(pool, struct managesieve_url, 1);
+	url_parser.flags = flags;
+
+	if (managesieve_url_do_parse(&url_parser) < 0) {
+		i_assert(url_parser.parser.error != NULL);
+		*error_r = url_parser.parser.error;
+		return -1;
+	}
+
+	if (url_r != NULL)
+		*url_r = url_parser.url;
+	return 0;
+}
+
+/*
+ * Sieve URL manipulation
+ */
+
+void managesieve_url_init_authority_from(struct managesieve_url *dest,
+					 const struct managesieve_url *src)
+{
+	i_zero(dest);
+	dest->host = src->host;
+	dest->port = src->port;
+}
+
+void managesieve_url_copy_authority(pool_t pool, struct managesieve_url *dest,
+				    const struct managesieve_url *src)
+{
+	i_zero(dest);
+	uri_host_copy(pool, &dest->host, &src->host);
+	dest->port = src->port;
+}
+
+struct managesieve_url *
+managesieve_url_clone_authority(pool_t pool, const struct managesieve_url *src)
+{
+	struct managesieve_url *new_url;
+
+	new_url = p_new(pool, struct managesieve_url, 1);
+	managesieve_url_copy_authority(pool, new_url, src);
+
+	return new_url;
+}
+
+void managesieve_url_copy(pool_t pool, struct managesieve_url *dest,
+			  const struct managesieve_url *src)
+{
+	managesieve_url_copy_authority(pool, dest, src);
+	dest->owner = p_strdup(pool, src->owner);
+	dest->scriptname = p_strdup(pool, src->scriptname);
+}
+
+void managesieve_url_copy_with_userinfo(pool_t pool,
+					struct managesieve_url *dest,
+					const struct managesieve_url *src)
+{
+	managesieve_url_copy(pool, dest, src);
+	dest->user = p_strdup(pool, src->user);
+	dest->password = p_strdup(pool, src->password);
+}
+
+struct managesieve_url *
+managesieve_url_clone(pool_t pool, const struct managesieve_url *src)
+{
+	struct managesieve_url *new_url;
+
+	new_url = p_new(pool, struct managesieve_url, 1);
+	managesieve_url_copy(pool, new_url, src);
+
+	return new_url;
+}
+
+struct managesieve_url *
+managesieve_url_clone_with_userinfo(pool_t pool,
+				    const struct managesieve_url *src)
+{
+	struct managesieve_url *new_url;
+
+	new_url = p_new(pool, struct managesieve_url, 1);
+	managesieve_url_copy_with_userinfo(pool, new_url, src);
+
+	return new_url;
+}
+
+/*
+ * Sieve URL construction
+ */
+
+static void
+managesieve_url_add_scheme(string_t *urlstr)
+{
+	/* scheme */
+	uri_append_scheme(urlstr, "sieve");
+	str_append(urlstr, "//");
+}
+
+static void
+managesieve_url_add_authority(string_t *urlstr,
+			      const struct managesieve_url *url)
+{
+	/* userinfo */
+	if (url->user != NULL) {
+		if (url->user != NULL)
+			uri_append_user_data(urlstr, ";:", url->user);
+		str_append_c(urlstr, '@');
+	}
+	/* host */
+	uri_append_host(urlstr, &url->host);
+	/* port */
+	if (url->port != MANAGESIEVE_DEFAULT_PORT)
+		uri_append_port(urlstr, url->port);
+}
+
+static void
+managesieve_url_add_path(string_t *urlstr, const struct managesieve_url *url)
+{
+	if (url->owner == NULL) {
+		if (url->scriptname == NULL)
+			return;
+	} else {
+		i_assert(url->scriptname != NULL && *url->scriptname != '\0');
+
+		str_append_c(urlstr, '/');
+		uri_append_path_segment_data(urlstr, ";&=", url->owner);
+	}
+
+	str_append_c(urlstr, '/');
+	uri_append_path_segment_data(urlstr, ";&=", url->scriptname);
+}
+
+const char *managesieve_url_create(const struct managesieve_url *url)
+{
+	string_t *urlstr = t_str_new(512);
+
+	managesieve_url_add_scheme(urlstr);
+	managesieve_url_add_authority(urlstr, url);
+	managesieve_url_add_path(urlstr, url);
+
+	return str_c(urlstr);
+}
+
+const char *managesieve_url_create_host(const struct managesieve_url *url)
+{
+	string_t *urlstr = t_str_new(512);
+
+	managesieve_url_add_scheme(urlstr);
+	managesieve_url_add_authority(urlstr, url);
+
+	return str_c(urlstr);
+}
+
+const char *managesieve_url_create_authority(const struct managesieve_url *url)
+{
+	string_t *urlstr = t_str_new(256);
+
+	managesieve_url_add_authority(urlstr, url);
+
+	return str_c(urlstr);
+}
diff --git a/src/lib-managesieve/managesieve-url.h b/src/lib-managesieve/managesieve-url.h
new file mode 100644
index 000000000..37b3dce4d
--- /dev/null
+++ b/src/lib-managesieve/managesieve-url.h
@@ -0,0 +1,89 @@
+#ifndef MANAGESIEVE_URL_H
+#define MANAGESIEVE_URL_H
+
+#include "net.h"
+#include "uri-util.h"
+
+#include "managesieve-protocol.h"
+
+struct managesieve_url {
+	/* server */
+	struct uri_host host;
+	in_port_t port;
+
+	/* userinfo (not parsed by default) */
+	const char *user;
+	const char *password;
+
+	/* path */
+	const char *owner;
+	const char *scriptname;
+};
+
+/*
+ * Sieve URL parsing
+ */
+
+enum managesieve_url_parse_flags {
+	/* Scheme part 'sieve:' is already parsed externally. This implies that
+	   this is an absolute SIEVE URL. */
+	MANAGESIEVE_URL_PARSE_SCHEME_EXTERNAL	= 0x01,
+	/* Allow 'user:password@' part in SIEVE URL */
+	MANAGESIEVE_URL_ALLOW_USERINFO_PART	= 0x04,
+};
+
+int managesieve_url_parse(const char *url,
+			  enum managesieve_url_parse_flags flags, pool_t pool,
+			  struct managesieve_url **url_r, const char **error_r)
+			  ATTR_NULL(4);
+
+/*
+ * Sieve URL evaluation
+ */
+
+static inline in_port_t
+managesieve_url_get_port_default(const struct managesieve_url *url,
+				 in_port_t default_port)
+{
+	return (url->port != 0 ? url->port : default_port);
+}
+
+static inline in_port_t
+managesieve_url_get_port(const struct managesieve_url *url)
+{
+	return managesieve_url_get_port_default(url, MANAGESIEVE_DEFAULT_PORT);
+}
+
+/*
+ * Sieve URL manipulation
+ */
+
+void managesieve_url_init_authority_from(struct managesieve_url *dest,
+					 const struct managesieve_url *src);
+void managesieve_url_copy_authority(pool_t pool, struct managesieve_url *dest,
+				    const struct managesieve_url *src);
+struct managesieve_url *
+managesieve_url_clone_authority(pool_t pool, const struct managesieve_url *src);
+
+void managesieve_url_copy(pool_t pool, struct managesieve_url *dest,
+			  const struct managesieve_url *src);
+void managesieve_url_copy_with_userinfo(pool_t pool,
+					struct managesieve_url *dest,
+					const struct managesieve_url *src);
+
+struct managesieve_url *
+managesieve_url_clone(pool_t pool,const struct managesieve_url *src);
+struct managesieve_url *
+managesieve_url_clone_with_userinfo(pool_t pool,
+				    const struct managesieve_url *src);
+
+/*
+ * Sieve URL construction
+ */
+
+const char *managesieve_url_create(const struct managesieve_url *url);
+
+const char *managesieve_url_create_host(const struct managesieve_url *url);
+const char *managesieve_url_create_authority(const struct managesieve_url *url);
+
+#endif
diff --git a/src/lib-managesieve/test-managesieve-url.c b/src/lib-managesieve/test-managesieve-url.c
new file mode 100644
index 000000000..d1c8a6328
--- /dev/null
+++ b/src/lib-managesieve/test-managesieve-url.c
@@ -0,0 +1,345 @@
+/* Copyright (c) 2021 Pigeonhole authors, see the included COPYING file
+ */
+
+#include "lib.h"
+#include "net.h"
+#include "managesieve-url.h"
+#include "test-common.h"
+
+struct valid_managesieve_url_test {
+	const char *url;
+	enum managesieve_url_parse_flags flags;
+
+	struct managesieve_url url_parsed;
+};
+
+/* Valid MANAGESIEVE URL tests */
+static struct valid_managesieve_url_test valid_url_tests[] = {
+	/* Generic tests */
+	{
+		.url = "sieve://localhost",
+		.url_parsed = {
+			.host = { .name = "localhost" },
+		},
+	},
+	{
+		.url = "sieve://www.%65%78%61%6d%70%6c%65.com",
+		.url_parsed = {
+			.host = { .name = "www.example.com" },
+		},
+	},
+	{
+		.url = "sieve://www.dovecot.org:8080",
+		.url_parsed = {
+			.host = { .name = "www.dovecot.org" },
+			.port = 8080,
+		},
+	},
+	{
+		.url = "sieve://127.0.0.1",
+		.url_parsed = {
+			.host = {
+				.name = "127.0.0.1",
+				.ip = { .family = AF_INET },
+			},
+		},
+	},
+	{
+		.url = "sieve://[::1]",
+		.url_parsed = {
+			.host = {
+				.name = "[::1]",
+				.ip = { .family = AF_INET6 },
+			},
+		},
+	},
+	{
+		.url = "sieve://[::1]:8080",
+		.url_parsed = {
+			.host = {
+				.name = "[::1]",
+				.ip = { .family = AF_INET6 },
+			},
+			.port = 8080,
+		},
+	},
+	{
+		.url = "sieve://user@api.dovecot.org",
+		.flags = MANAGESIEVE_URL_ALLOW_USERINFO_PART,
+		.url_parsed = {
+			.host = { .name = "api.dovecot.org" },
+			.user = "user",
+		},
+	},
+	{
+		.url = "sieve://userid:secret@api.dovecot.org",
+		.flags = MANAGESIEVE_URL_ALLOW_USERINFO_PART,
+		.url_parsed = {
+			.host = { .name = "api.dovecot.org" },
+			.user = "userid",
+			.password = "secret",
+		},
+	},
+	{
+		.url = "sieve://su%3auserid:secret@api.dovecot.org",
+		.flags = MANAGESIEVE_URL_ALLOW_USERINFO_PART,
+		.url_parsed = {
+			.host = { .name = "api.dovecot.org" },
+			.user = "su:userid",
+			.password = "secret",
+		},
+	},
+	{
+		.url = "sieve://www.example.com/",
+		.url_parsed = {
+			.host = { .name = "www.example.com" },
+			.scriptname = "",
+		},
+	},
+	{
+		.url = "sieve://www.example.com/frop",
+		.url_parsed = {
+			.host = { .name = "www.example.com" },
+			.scriptname = "frop",
+		},
+	},
+	{
+		.url = "sieve://www.example.com/user/frop",
+		.url_parsed = {
+			.host = { .name = "www.example.com" },
+			.owner = "user",
+			.scriptname = "frop",
+		},
+	},
+};
+
+static unsigned int valid_url_test_count = N_ELEMENTS(valid_url_tests);
+
+static void
+test_managesieve_url_equal(struct managesieve_url *urlt, struct managesieve_url *urlp)
+{
+	if (urlp->host.name == NULL || urlt->host.name == NULL) {
+		test_assert(urlp->host.name == urlt->host.name);
+	} else {
+		test_assert(strcmp(urlp->host.name, urlt->host.name) == 0);
+	}
+	test_assert(urlp->port == urlt->port);
+	test_assert(urlp->host.ip.family == urlt->host.ip.family);
+	if (urlp->user == NULL || urlt->user == NULL) {
+		test_assert(urlp->user == urlt->user);
+	} else {
+		test_assert(strcmp(urlp->user, urlt->user) == 0);
+	}
+	if (urlp->password == NULL || urlt->password == NULL) {
+		test_assert(urlp->password == urlt->password);
+	} else {
+		test_assert(strcmp(urlp->password, urlt->password) == 0);
+	}
+	if (urlp->owner == NULL || urlt->owner == NULL) {
+		test_assert(urlp->owner == urlt->owner);
+	} else {
+		test_assert(strcmp(urlp->owner, urlt->owner) == 0);
+	}
+	if (urlp->scriptname == NULL || urlt->scriptname == NULL) {
+		test_assert(urlp->scriptname == urlt->scriptname);
+	} else {
+		test_assert(strcmp(urlp->scriptname, urlt->scriptname) == 0);
+	}
+}
+
+static void test_managesieve_url_valid(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < valid_url_test_count; i++) T_BEGIN {
+		const char *url = valid_url_tests[i].url;
+		enum managesieve_url_parse_flags flags =
+			valid_url_tests[i].flags;
+		struct managesieve_url *urlt = &valid_url_tests[i].url_parsed;
+		struct managesieve_url *urlp;
+		const char *error = NULL;
+
+		test_begin(t_strdup_printf("managesieve url valid [%d]", i));
+
+		if (managesieve_url_parse(url, flags,
+					  pool_datastack_create(),
+					  &urlp, &error) < 0)
+			urlp = NULL;
+
+		test_out_reason(t_strdup_printf("managesieve_url_parse(%s)",
+				valid_url_tests[i].url), urlp != NULL, error);
+		if (urlp != NULL)
+			test_managesieve_url_equal(urlt, urlp);
+
+		test_end();
+	} T_END;
+}
+
+struct invalid_managesieve_url_test {
+	const char *url;
+	enum managesieve_url_parse_flags flags;
+	struct managesieve_url url_base;
+};
+
+static struct invalid_managesieve_url_test invalid_url_tests[] = {
+	{
+		.url = "imap://example.com/INBOX"
+	},
+	{
+		.url = "managesieve:/www.example.com"
+	},
+	{
+		.url = ""
+	},
+	{
+		.url = "/frop"
+	},
+	{
+		.url = "sieve//www.example.com/frop\""
+	},
+	{
+		.url = "sieve:///dovecot.org"
+	},
+	{
+		.url = "sieve://[]/frop"
+	},
+	{
+		.url = "sieve://[v08.234:232:234:234:2221]/user/frop"
+	},
+	{
+		.url = "sieve://[1::34a:34:234::6]/frop"
+	},
+	{
+		.url = "sieve://example%a.com/frop"
+	},
+	{
+		.url = "sieve://example.com%/user/frop"
+	},
+	{
+		.url = "sieve://example%00.com/frop"
+	},
+	{
+		.url = "sieve://example.com:65536/frop"
+	},
+	{
+		.url = "sieve://example.com:72817/frop"
+	},
+	{
+		.url = "sieve://example.com/user/%00"
+	},
+	{
+		.url = "sieve://example.com/user/%0r"
+	},
+	{
+		.url = "sieve://example.com/user/%"
+	},
+	{
+		.url = "sieve://example.com/?%00"
+	},
+	{
+		.url = "sieve://example.com/user/"
+	},
+	{
+		.url = "sieve://example.com/user/frop/frml"
+	},
+	{
+		.url = "sieve://www.example.com/user/frop#IMAP_Server"
+	},
+	{
+		.url = "sieve://example.com/#%00",
+	},
+	{
+		.url = "sieve://example.com/?query"
+	},
+	{
+		.url = "sieve://example.com/user?query"
+	},
+	{
+		.url = "sieve://example.com/?query/user"
+	},
+	{
+		.url = "sieve://example.com/#fragment"
+	},
+	{
+		.url = "sieve://example.com/user#fragment"
+	},
+	{
+		.url = "sieve://example.com/#fragment/user"
+	},
+};
+
+static unsigned int invalid_url_test_count = N_ELEMENTS(invalid_url_tests);
+
+static void test_managesieve_url_invalid(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < invalid_url_test_count; i++) T_BEGIN {
+		const char *url = invalid_url_tests[i].url;
+		enum managesieve_url_parse_flags flags = invalid_url_tests[i].flags;
+		struct managesieve_url *urlp;
+		const char *error = NULL;
+
+		test_begin(t_strdup_printf("managesieve url invalid [%d]", i));
+
+		if (managesieve_url_parse(url, flags, pool_datastack_create(),
+					  &urlp, &error) < 0)
+			urlp = NULL;
+		test_out_reason(t_strdup_printf("parse %s", url),
+				urlp == NULL, error);
+
+		test_end();
+	} T_END;
+
+}
+
+static const char *parse_create_url_tests[] = {
+	"sieve://www.example.com/",
+	"sieve://10.0.0.1/",
+	"sieve://[::1]/",
+	"sieve://www.example.com:993/",
+	"sieve://www.example.com/frop",
+	"sieve://www.example.com/user/frop",
+	"sieve://www.example.com/user/%23shared",
+};
+
+static unsigned int
+parse_create_url_test_count = N_ELEMENTS(parse_create_url_tests);
+
+static void test_managesieve_url_parse_create(void)
+{
+	unsigned int i;
+
+	for (i = 0; i < parse_create_url_test_count; i++) T_BEGIN {
+		const char *url = parse_create_url_tests[i];
+		struct managesieve_url *urlp;
+		const char *error = NULL;
+
+		test_begin(t_strdup_printf("managesieve url parse/create [%d]", i));
+
+		if (managesieve_url_parse(url, 0, pool_datastack_create(),
+					  &urlp, &error) < 0)
+			urlp = NULL;
+		test_out_reason(t_strdup_printf("parse  %s", url),
+				urlp != NULL, error);
+		if (urlp != NULL) {
+			const char *urlnew = managesieve_url_create(urlp);
+			test_out(t_strdup_printf("create %s", urlnew),
+				 strcmp(url, urlnew) == 0);
+		}
+
+		test_end();
+	} T_END;
+
+}
+
+int main(void)
+{
+	static void (*const test_functions[])(void) = {
+		test_managesieve_url_valid,
+		test_managesieve_url_invalid,
+		test_managesieve_url_parse_create,
+		NULL
+	};
+	return test_run(test_functions);
+}
-- 
GitLab