unofficial mirror of notmuch@notmuchmail.org
 help / color / mirror / code / Atom feed
From: Adrian Perez de Castro <aperez@igalia.com>
To: notmuch@notmuchmail.org
Subject: Re: SWIG (and particularly Python) bindings
Date: Wed, 30 Dec 2009 11:52:23 +0100	[thread overview]
Message-ID: <20091230115223.1b3472a1@hikari> (raw)
In-Reply-To: <1262078148-sup-7891@ben-laptop>


[-- Attachment #1.1: Type: text/plain, Size: 2425 bytes --]

On Tue, 29 Dec 2009 04:16:43 -0500, Ben wrote:

> Regardless, I thought it might be nice to have access to the notmuch
> backend from a language other than C (preferably my high-level language
> of choice, python) [...]

Funny, I was just doing the same: a Python binding. Haha, so now we have
two just-backed Python bindings. What should we do?

> [...] To this end, I took a few hours today acquainting
> myself with SWIG and produced these bindings for Python. Unfortunately,
> it doesn't appear that SWIG has particularly good support for
> object-oriented C [...]

I already used SWIG sometimes in the past (and did not like it a lot), so
my binding is using Cython [*] (which is exactly like Pyrex plus some extra
features), so the binding is partly manual.

> While the bindings are currently in the form of a patch to notmuch
> (creating a top-level swig directory in the source tree), they could
> certainly be moved out-of-tree if the powers that be don't feel it
> appropriate to include them. [...]

Same here, see attached patch. It is currently unfinished, and I was just
about to add support for iterating notmuch_threads_t and other similar
structures. I can also publish a Git repo with the entire branch, just
drop me a line if you want me to do that.

> [...] Unfortunately, the build system is currently almost entirely
> independent from the notmuch build system. If  these are to be
> included in-tree, I would be curious to hear people have
> to say about how we might integrate it into the sup build system.
                                                  ^^^
(Mmmh, I suppose you mean "notmuch build system" there :P)

Mine is a little more cooked, as I have added a distutils "setup.py"
script. The bad news is that Python modules need to be compiled as
relocatable object files (-fPIC to teh rescue!), and the linker will
refuse to link the generated code with "notmuch.a" -- so I am instructing
distutils to compile *all* sources again. Not nice.

BTW, I think that if more bindings start to appear, Notmuch might be built
as a shared library, to avoid duplicating it everywhere. One option may be
using *just* libtool but not the rest of auto-foo tools (for the record:
I agree with Carl that they are slow and wicked).

Regards,


[*] http://www.cython.org/
-- 
Adrian Perez de Castro <aperez@igalia.com>
Igalia - Free Software Engineering

[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #1.2: notmuch-python-wip.patch --]
[-- Type: text/x-patch, Size: 17456 bytes --]

 Makefile              |    1 +
 python/.gitignore     |    2 +
 python/Makefile       |    6 +
 python/Makefile.local |   15 ++
 python/notmuch.pyx    |  397 +++++++++++++++++++++++++++++++++++++++++++++++++
 python/pyutil.h       |   16 ++
 python/setup.py       |   89 +++++++++++
 7 files changed, 526 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile
index 021fdb8..081d670 100644
--- a/Makefile
+++ b/Makefile
@@ -37,6 +37,7 @@ include Makefile.config
 
 include lib/Makefile.local
 include compat/Makefile.local
+include python/Makefile.local
 include Makefile.local
 
 # The user has not set any verbosity, default to quiet mode and inform the
diff --git a/python/.gitignore b/python/.gitignore
new file mode 100644
index 0000000..7f0efa8
--- /dev/null
+++ b/python/.gitignore
@@ -0,0 +1,2 @@
+notmuch.c
+build/
diff --git a/python/Makefile b/python/Makefile
new file mode 100644
index 0000000..e1e5c43
--- /dev/null
+++ b/python/Makefile
@@ -0,0 +1,6 @@
+
+all: python
+
+%:
+	make -C .. $@
+
diff --git a/python/Makefile.local b/python/Makefile.local
new file mode 100644
index 0000000..140a701
--- /dev/null
+++ b/python/Makefile.local
@@ -0,0 +1,15 @@
+dir=python
+
+python: $(dir)/build/.stamp
+	(cd $(dir) && python setup.py build)
+	touch $@
+
+$(dir)/build/.stamp: lib/notmuch.a
+
+clean: clean-python
+
+clean-python:
+	$(RM) -r $(dir)/build
+
+.PHONY: clean-python python
+
diff --git a/python/notmuch.pyx b/python/notmuch.pyx
new file mode 100644
index 0000000..f38b719
--- /dev/null
+++ b/python/notmuch.pyx
@@ -0,0 +1,397 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim: fenc=utf-8 ft=pyrex
+#
+# Copyright © 2009 Adrian Perez <aperez@igalia.com>
+#
+# Distributed under terms of the GPLv3 license.
+#
+
+cdef extern from "talloc.h":
+	void* talloc_init(char *fmt, ...)
+	int   talloc_free(void *ctx)
+
+
+cdef extern from "pyutil.h":
+	#
+	# Utility macros
+	#
+	char** pyutil_alloc_strv(void *ctx, unsigned nitems)
+
+
+cdef extern from "notmuch.h":
+	#
+	# Return status handling
+	#
+	ctypedef enum notmuch_status_t:
+		NOTMUCH_STATUS_SUCCESS
+		NOTMUCH_STATUS_OUT_OF_MEMORY
+		NOTMUCH_STATUS_READONLY_DATABASE
+		NOTMUCH_STATUS_XAPIAN_EXCEPTION
+		NOTMUCH_STATUS_FILE_ERROR
+		NOTMUCH_STATUS_FILE_NOT_EMAIL
+		NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID
+		NOTMUCH_STATUS_NULL_POINTER
+		NOTMUCH_STATUS_TAG_TOO_LONG
+		NOTMUCH_STATUS_UNBALANCED_FREEZE_THAW
+
+	char* notmuch_status_to_string(notmuch_status_t status)
+
+	#
+	# notmuch_database_* -> notmuch.Database
+	#
+	ctypedef enum notmuch_database_mode_t:
+		NOTMUCH_DATABASE_MODE_READ_ONLY
+		NOTMUCH_DATABASE_MODE_READ_WRITE
+
+	ctypedef enum notmuch_sort_t:
+		NOTMUCH_SORT_OLDEST_FIRST
+		NOTMUCH_SORT_NEWEST_FIRST
+		NOTMUCH_SORT_MESSAGE_ID
+
+	ctypedef struct notmuch_database_t
+	ctypedef struct notmuch_messages_t
+	ctypedef struct notmuch_message_t
+	ctypedef struct notmuch_threads_t
+	ctypedef struct notmuch_thread_t
+	ctypedef struct notmuch_query_t
+	ctypedef struct notmuch_tags_t
+	ctypedef int time_t
+
+	int notmuch_threads_has_more(notmuch_threads_t *threads)
+	void notmuch_threads_advance(notmuch_threads_t *threads)
+	void notmuch_threads_destroy(notmuch_threads_t *threads)
+	notmuch_thread_t* notmuch_threads_get(notmuch_threads_t *threads)
+
+	void   notmuch_thread_destroy(notmuch_thread_t *thread)
+	char*  notmuch_thread_get_authors(notmuch_thread_t *thread)
+	char*  notmuch_thread_get_subject(notmuch_thread_t *thread)
+	char*  notmuch_thread_get_thread_id(notmuch_thread_t *thread)
+	time_t notmuch_thread_get_oldest_date(notmuch_thread_t *thread)
+	time_t notmuch_thread_get_newest_date(notmuch_thread_t *thread)
+	int    notmuch_thread_get_total_messages(notmuch_thread_t *thread)
+	int    notmuch_thread_get_matched_messages(notmuch_thread_t *thread)
+	notmuch_tags_t*     notmuch_thread_get_tags(notmuch_thread_t *thread)
+	notmuch_messages_t* notmuch_thread_get_toplevel_messages(notmuch_thread_t *thread)
+
+	notmuch_database_t* notmuch_database_create(char *path)
+	notmuch_database_t* notmuch_database_open(char *path, notmuch_database_mode_t mode)
+	char* notmuch_database_get_path(notmuch_database_t *db)
+	void  notmuch_database_close(notmuch_database_t *db)
+
+	time_t notmuch_database_get_timestamp(notmuch_database_t *db, char *key)
+	notmuch_status_t notmuch_database_set_timestamp(
+			notmuch_database_t *db, char *key, time_t timestamp)
+
+	notmuch_status_t notmuch_database_add_message(
+			notmuch_database_t *db, char *filename, notmuch_message_t **mesage)
+
+	notmuch_message_t* notmuch_database_find_message(
+			notmuch_database_t *db, char *message_id)
+
+	notmuch_tags_t* notmuch_database_get_all_tags(notmuch_database_t *db)
+
+	notmuch_query_t* notmuch_query_create(notmuch_database_t *db, char *qstring)
+
+	void notmuch_query_destroy(notmuch_query_t *query)
+	void notmuch_query_set_sort(notmuch_query_t *query, notmuch_sort_t sort)
+	unsigned notmuch_query_count_messages(notmuch_query_t *query)
+	notmuch_threads_t*  notmuch_query_search_threads(notmuch_query_t *query)
+	notmuch_messages_t* notmuch_query_search_messages(notmuch_query_t *query)
+
+	char* notmuch_message_get_message_id(notmuch_message_t *msg)
+	char* notmuch_message_get_thread_id(notmuch_message_t *msg)
+	char* notmuch_message_get_filename(notmuch_message_t *msg)
+	char* notmuch_message_get_header(notmuch_message_t *msg, char *name)
+	notmuch_status_t notmuch_message_add_tag(notmuch_message_t *msg, char *tag)
+	notmuch_status_t notmuch_message_remove_tag(notmuch_message_t *msg, char *tag)
+	void notmuch_message_remove_all_tags(notmuch_message_t *msg)
+	void notmuch_message_destroy(notmuch_message_t *msg)
+	void notmuch_message_freeze(notmuch_message_t *msg)
+	void notmuch_message_thaw(notmuch_message_t *msg)
+
+
+cdef extern from "notmuch-client.h":
+	#
+	# notmuch_config_* -> notmuch.Config
+	#
+	ctypedef struct notmuch_config_t
+
+	notmuch_config_t* notmuch_config_open(void *ctx, char *filename, int *is_new_ret)
+	void   notmuch_config_close(notmuch_config_t *cfg)
+	int    notmuch_config_save(notmuch_config_t *cfg)
+	char*  notmuch_config_get_database_path(notmuch_config_t *cfg)
+	void   notmuch_config_set_database_path(notmuch_config_t *cfg, char *path)
+	char*  notmuch_config_get_user_name(notmuch_config_t *cfg)
+	void   notmuch_config_set_user_name(notmuch_config_t *cfg, char *name)
+	char*  notmuch_config_get_user_primary_email(notmuch_config_t *cfg)
+	void   notmuch_config_set_user_primary_email(notmuch_config_t *cfg, char *email)
+	char** notmuch_config_get_user_other_email(notmuch_config_t *cfg, size_t *length)
+	void   notmuch_config_set_user_other_email(notmuch_config_t *cfg, char **other_email, size_t length)
+
+	#
+	# Miscellaneous
+	#
+	int debugger_is_active()
+
+
+#
+# Import needed Python built-in modules
+#
+from datetime import datetime, date
+
+#
+# Miscellaneous functions and information
+#
+debugger_active = bool(debugger_is_active())
+
+
+#
+# notmuch_database_* -> notmuch.Database
+#
+
+
+#
+# notmuch_config_* -> notmuch.Config
+#
+cdef class Config:
+	"""Handles the Notmuch configuration file.
+	"""
+	cdef notmuch_config_t * _cfg
+	cdef void * _ctx
+	cdef object _filename
+
+	def __init__(self, filename="~/.notmuch-config"):
+		"""Open a Notmuch configuration file.
+		"""
+		cdef int newret
+		self._ctx = talloc_init("notmuch.Config")
+		self._cfg = notmuch_config_open(self._ctx, filename, &newret)
+		self._filename = filename
+
+	def __dealloc__(self):
+		notmuch_config_close(self._cfg)
+		talloc_free(self._ctx)
+
+	property filename:
+		"""File name containing the configuration (string)"""
+		def __get__(self):
+			return self._filename
+
+	property database_path:
+		"""Path to the Notmuch database (string)"""
+		def __get__(self):
+			return notmuch_config_get_database_path(self._cfg)
+		def __set__(self, path):
+			notmuch_config_set_database_path(self._cfg, path)
+
+	property user_name:
+		"""User name (string)"""
+		def __get__(self):
+			return notmuch_config_get_user_name(self._cfg)
+		def __set__(self, name):
+			notmuch_config_set_user_name(self._cfg, name)
+
+	property user_primary_email:
+		"""Primary e-mail address of the user (string)"""
+		def __get__(self):
+			return notmuch_config_get_user_primary_email(self._cfg)
+		def __set__(self, email):
+			notmuch_config_set_user_primary_email(self._cfg, email)
+
+	property other_email:
+		"""List of other e-mail addresses of the user (tuple of strings)"""
+		def __get__(self):
+			cdef size_t length
+			cdef size_t i
+			cdef char **emails
+			emails = notmuch_config_get_user_other_email(self._cfg, &length)
+
+			result = []
+			for i from 0 <= i < length:
+				result.append(emails[i])
+
+			# XXX We do not want the result to be modifiable, because the property
+			#     must be assigned as a whole, and not just modified only in the
+			#     Python side of the world.
+			return tuple(result)
+
+		def __set__(self, emaillist):
+			cdef size_t length = len(emaillist)
+			cdef char **emails = pyutil_alloc_strv(self._ctx, length)
+			cdef size_t i
+
+			for i from 0 <= i < len(emaillist):
+				emails[i] = emaillist[i]
+			notmuch_config_set_user_other_email(self._cfg, emails, len(emaillist))
+
+
+	def save(self):
+		"""Save the Notmuch configuration"""
+		notmuch_config_save(self._cfg)
+
+
+cdef class Database
+
+
+cdef class Message:
+	cdef notmuch_message_t *_msg
+
+	def __cinit__(self, object messageptr):
+		# XXX Counterpart of bogus cast
+		self._msg = <notmuch_message_t*> messageptr
+
+	def __dealloc__(self):
+		notmuch_message_destroy(self._msg)
+
+	property message_id:
+		def __get__(self):
+			return notmuch_message_get_message_id(self._msg)
+
+	property thread_id:
+		def __get__(self):
+			return notmuch_message_get_thread_id(self._msg)
+
+	property filename:
+		def __get__(self):
+			return notmuch_message_get_filename(self._msg)
+
+	def get_header(self, name):
+		return notmuch_message_get_header(self._msg, name)
+
+	def add_tag(self, tag):
+		cdef notmuch_status_t ret = notmuch_message_add_tag(self._msg, tag)
+		if ret != NOTMUCH_STATUS_SUCCESS:
+			raise ValueError(notmuch_status_to_string(ret))
+
+	def remove_tag(self, tag):
+		cdef notmuch_status_t ret = notmuch_message_remove_tag(self._msg, tag)
+		if ret != NOTMUCH_STATUS_SUCCESS:
+			raise ValueError(notmuch_status_to_string(ret))
+
+	def remove_all_tags(self):
+		notmuch_message_remove_all_tags(self._msg)
+
+	def freeze(self):
+		notmuch_message_freeze(self._msg)
+
+	def thaw(self):
+		notmuch_message_thaw(self._msg)
+
+
+cdef class Thread:
+	cdef notmuch_thread_t *_thread
+
+	def __cinit__(self, object threadptr):
+		self._thread = <notmuch_thread_t*> threadptr
+
+	def __dealloc__(self):
+		notmuch_thread_destroy(self._thread)
+
+	property authors:
+		def __get__(self):
+			return notmuch_thread_get_authors(self._thread)
+
+	property subject:
+		def __get__(self):
+			return notmuch_thread_get_subject(self._thread)
+
+	property thread_id:
+		def __get__(self):
+			return notmuch_thread_get_thread_id(self._thread)
+
+	property oldest_date:
+		def __get__(self):
+			return datetime.fromtimestamp(notmuch_thread_get_oldest_date(self._thread))
+
+	property newest_date:
+		def __get__(self):
+			return datetime.fromtimestamp(notmuch_thread_get_newest_date(self._thread))
+
+	property total_messages:
+		def __get__(self):
+			return notmuch_thread_get_total_messages(self._thread)
+
+	property matched_messages:
+		def __get__(self):
+			return notmuch_thread_get_matched_messages(self._thread)
+
+	def __len__(self):
+		return self.matched_messages
+
+
+cdef class Query:
+	cdef notmuch_query_t *_query
+
+	def __cinit__(self, Database db not None, qs):
+		self._query = notmuch_query_create(db._db, qs)
+
+	def __dealloc__(self):
+		notmuch_query_destroy(self._query)
+
+	def __len__(self):
+		return notmuch_query_count_messages(self._query)
+
+	def sort(self, notmuch_sort_t ordering):
+		notmuch_query_set_sort(self._query, ordering)
+		return self
+
+
+
+cdef class Database:
+	cdef notmuch_database_t *_db
+
+	def __init__(self, path=None, readonly=True):
+		cdef notmuch_database_mode_t mode = NOTMUCH_DATABASE_MODE_READ_WRITE
+		if path is None:
+			path = Config().database_path
+		if readonly:
+			mode = NOTMUCH_DATABASE_MODE_READ_ONLY
+		self._db = notmuch_database_open(path, mode)
+
+	def __dealloc__(self):
+		notmuch_database_close(self._db)
+
+	def get_timestamp(self, key):
+		cdef time_t ts = notmuch_database_get_timestamp(self._db, key)
+		return datetime.fromtimestamp(ts)
+
+	def set_timestamp(self, key, timestamp):
+		cdef time_t ts
+		if isinstance(timestamp, date):
+			ts = int(timestamp.strftime("%s"))
+		elif isinstance(timestamp, float):
+			ts = <time_t> timestamp
+		elif isinstance(timestamp, int):
+			ts = timestamp
+		else:
+			raise ValueError("Numeric timestamp or datetime.date object expected")
+
+	def add_message(self, filename):
+		cdef notmuch_message_t *message = NULL
+		cdef notmuch_status_t   status
+		status = notmuch_database_add_message(self._db, filename, &message)
+		if status == NOTMUCH_STATUS_SUCCESS:
+			# XXX This cast seems bogus, it may work, though
+			return Message(<object> message)
+		else:
+			if message != NULL:
+				notmuch_message_destroy(message)
+			raise ValueError(notmuch_status_to_string(status))
+
+	def find_message(self, message_id):
+		cdef notmuch_message_t *message
+		message = notmuch_database_find_message(self._db, message_id)
+		if message == NULL:
+			raise KeyError(message_id)
+		return Message(<object> message)
+
+	property path:
+		"""Database path"""
+		def __get__(self):
+			return notmuch_database_get_path(self._db)
+
+	def query(self, qstring):
+		return Query(self, qstring)
+
+
diff --git a/python/pyutil.h b/python/pyutil.h
new file mode 100644
index 0000000..64d93bf
--- /dev/null
+++ b/python/pyutil.h
@@ -0,0 +1,16 @@
+/*
+ * pyutil.h
+ * Copyright (C) 2009 Adrian Perez <aperez@igalia.com>
+ *
+ * Distributed under terms of the GPLv3 license.
+ */
+
+#ifndef __pyutil_h__
+#define __pyutil_h__
+
+#include <talloc.h>
+
+#define pyutil_alloc_strv(_ctx, _n)  talloc_array ((_ctx), char*, (_n))
+
+#endif /* !__pyutil_h__ */
+
diff --git a/python/setup.py b/python/setup.py
new file mode 100644
index 0000000..ffd43b2
--- /dev/null
+++ b/python/setup.py
@@ -0,0 +1,89 @@
+#! /usr/bin/env python
+# -*- coding: utf-8 -*-
+# vim:fenc=utf-8
+#
+# Copyright © 2009 Adrian Perez <aperez@igalia.com>
+#
+# Distributed under terms of the GPLv3 license.
+
+from distutils.core import setup
+from distutils.extension import Extension
+from Cython.Distutils import build_ext
+import commands
+
+
+class FlagOMatic(dict):
+    _KEYS = ("extra_link_args", "include_dirs", "library_dirs", "libraries")
+
+    def __init__(self, *arg, **kw):
+        super(FlagOMatic, self).__init__(*arg, **kw)
+        for key in self._KEYS:
+            self[key] = set(self.get(key, []))
+
+    extra_link_args = property(lambda self: self["extra_link_args"])
+    include_dirs    = property(lambda self: self["include_dirs"])
+    library_dirs    = property(lambda self: self["library_dirs"])
+    libraries       = property(lambda self: self["libraries"])
+
+    _FLAG_MAP = {
+            "-I": "include_dirs",
+            "-L": "library_dirs",
+            "-l": "libraries",
+        }
+
+    def add_compiler_flags(self, text):
+        for token in text.split():
+            key = self._FLAG_MAP.get(token[:2], "extra_link_args")
+            if key == "extra_link_args":
+                self.extra_link_args.add(token)
+            else:
+                self[key].add(token[2:])
+
+    def add_pkgconfig_flags(self, *modules):
+        self.add_command_output_flags(
+                "pkg-config --libs --cflags %s" % " ".join(modules))
+
+    def add_command_output_flags(self, command):
+        self.add_compiler_flags(commands.getoutput(command))
+
+    @property
+    def kwargs(self):
+        return dict((k, list(v)) for k, v in self.iteritems())
+
+
+# Gather compiler flags
+#
+flags = FlagOMatic(
+       #extra_link_args = ("../lib/notmuch.a",),
+        include_dirs    = ("..", "../lib", "../compat"))
+
+flags.add_pkgconfig_flags("gmime-2.4", "talloc")
+flags.add_command_output_flags("xapian-config --cxxflags --libs")
+
+# We are building a extension module
+#
+import os
+
+srcs = ["notmuch.pyx"]
+srcs.extend(
+    map(lambda s: "../"+s,
+        filter(lambda s: s.endswith(".c"),
+            os.listdir(".."))))
+srcs.extend(
+    map(lambda s: "../lib/"+s,
+        filter(lambda s: s.endswith(".cc") or s.endswith(".c"),
+            os.listdir("../lib"))))
+
+ext_modules = [Extension("notmuch", srcs, **flags.kwargs)]
+
+
+# And now for the easy part :-)
+#
+setup(
+    name         = "notmuch",
+    author       = "Adrian Perez",
+    author_email = "aperez@igalia.com",
+    cmdclass     = {"build_ext": build_ext},
+    ext_modules  = ext_modules,
+)
+

[-- Attachment #2: signature.asc --]
[-- Type: application/pgp-signature, Size: 198 bytes --]

  reply	other threads:[~2009-12-30 10:51 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2009-12-29  9:16 SWIG (and particularly Python) bindings Ben Gamari
2009-12-30 10:52 ` Adrian Perez de Castro [this message]
2009-12-30 11:34   ` Scott Robinson
2010-01-16  4:09     ` Ben Gamari
2010-01-16  4:28       ` [PATCH] libtoolize notmuch Ben Gamari
2010-01-20  8:51         ` Carl Worth
2009-12-30 16:10   ` SWIG (and particularly Python) bindings Ben Gamari
2010-01-20  8:48   ` Carl Worth
2010-01-20  8:45 ` Carl Worth
2010-01-20 18:39   ` Ben Gamari

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

  List information: https://notmuchmail.org/

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20091230115223.1b3472a1@hikari \
    --to=aperez@igalia.com \
    --cc=notmuch@notmuchmail.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.
Code repositories for project(s) associated with this public inbox

	https://yhetil.org/notmuch.git/

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).