From: Wietse Z Venema Date: Thu, 8 Feb 2024 05:00:00 +0000 (-0500) Subject: postfix-3.9-20240208 X-Git-Tag: v3.9.0~9 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=478c124eafad24120abbe587adc3b851869a4143;p=thirdparty%2Fpostfix.git postfix-3.9-20240208 --- diff --git a/postfix/HISTORY b/postfix/HISTORY index a16d57d5c..fc0fa24c6 100644 --- a/postfix/HISTORY +++ b/postfix/HISTORY @@ -27810,3 +27810,19 @@ Apologies for any names omitted. Documentation: the post-install(1) manpage now lists $config_directory/makedefs.out as one of the installed files. File: postfix-install. + +20240208 + + Refactored the JSON string quoting function, so that it can + be shared between the postqueue command and the MongoDB + client implementation. Files: util.quote_for_json.c, + util/stringops.h, postqueue/showq_json.c. + + MongoDB client support, contributed by Hamid Maadani, based + on earlier code by Stephan Ferraro. Files: conf/dynamicmaps.cf, + conf/postfix-files, makedefs, mantools/postlink, + proto/DATABASE_README.html, proto/Makefile.in, + proto/MONGODB_README.html, proto/mongodb_table, + global/dict_mongodb.c, global/dict_mongodb.h, global/mail_dict.c, + global/Makefile.in, postconf/Makefile.in, proto/INSTALL.html, + postfix/postfix.c. diff --git a/postfix/INSTALL b/postfix/INSTALL index 5939a995a..90b1b2d8a 100644 --- a/postfix/INSTALL +++ b/postfix/INSTALL @@ -376,27 +376,29 @@ whistles. Support for third-party databases etc. must be configured when Postfix is compiled. The following documents describe how to build Postfix with support for optional features: - _____________________________________________________________ - |Optional feature |Document |Availability| - |__________________________________|_____________|____________| - |Berkeley DB database |DB_README |Postfix 1.0 | - |__________________________________|_____________|____________| - |LMDB database |LMDB_README |Postfix 2.11| - |__________________________________|_____________|____________| - |LDAP database |LDAP_README |Postfix 1.0 | - |__________________________________|_____________|____________| - |MySQL database |MYSQL_README |Postfix 1.0 | - |__________________________________|_____________|____________| - |Perl compatible regular expression|PCRE_README |Postfix 1.0 | - |__________________________________|_____________|____________| - |PostgreSQL database |PGSQL_README |Postfix 2.0 | - |__________________________________|_____________|____________| - |SASL authentication |SASL_README |Postfix 1.0 | - |__________________________________|_____________|____________| - |SQLite database |SQLITE_README|Postfix 2.8 | - |__________________________________|_____________|____________| - |STARTTLS session encryption |TLS_README |Postfix 2.2 | - |__________________________________|_____________|____________| + ______________________________________________________________ + |Optional feature |Document |Availability| + |__________________________________|______________|____________| + |Berkeley DB database |DB_README |Postfix 1.0 | + |__________________________________|______________|____________| + |LMDB database |LMDB_README |Postfix 2.11| + |__________________________________|______________|____________| + |LDAP database |LDAP_README |Postfix 1.0 | + |__________________________________|______________|____________| + |MongoDB database |MONGODB_README|Postfix 3.9 | + |__________________________________|______________|____________| + |MySQL database |MYSQL_README |Postfix 1.0 | + |__________________________________|______________|____________| + |Perl compatible regular expression|PCRE_README |Postfix 1.0 | + |__________________________________|______________|____________| + |PostgreSQL database |PGSQL_README |Postfix 2.0 | + |__________________________________|______________|____________| + |SASL authentication |SASL_README |Postfix 1.0 | + |__________________________________|______________|____________| + |SQLite database |SQLITE_README |Postfix 2.8 | + |__________________________________|______________|____________| + |STARTTLS session encryption |TLS_README |Postfix 2.2 | + |__________________________________|______________|____________| Note: IP version 6 support is compiled into Postfix on operating systems that have IPv6 support. See the IPV6_README file for details. diff --git a/postfix/README_FILES/AAAREADME b/postfix/README_FILES/AAAREADME index 9afa3b7d2..6bef06dec 100644 --- a/postfix/README_FILES/AAAREADME +++ b/postfix/README_FILES/AAAREADME @@ -52,6 +52,7 @@ LLooookkuupp ttaabblleess ((ddaattaabbaasseess)) * LDAP_README: LDAP Howto * LMDB_README: LMDB Howto * MEMCACHE_README: Memcache Howto + * MONGODB_README: MongoDB Howto * MYSQL_README: MySQL Howto * PCRE_README: PCRE Howto * PGSQL_README: PostgreSQL Howto diff --git a/postfix/README_FILES/DATABASE_README b/postfix/README_FILES/DATABASE_README index edf7c588f..f1629e925 100644 --- a/postfix/README_FILES/DATABASE_README +++ b/postfix/README_FILES/DATABASE_README @@ -236,6 +236,9 @@ To find out what database types your Postfix system supports, use the "ppooss mmeemmccaacchhee Memcache database client. Configuration details are given in memcache_table(5). + mmoonnggooddbb (read-only) + MongoDB database client. Configuration details are given in + mongodb_table(5), with examples in MONGODB_README. mmyyssqqll (read-only) MySQL database client. Configuration details are given in mysql_table (5). diff --git a/postfix/README_FILES/INSTALL b/postfix/README_FILES/INSTALL index 09d0f803e..85ed2cc63 100644 --- a/postfix/README_FILES/INSTALL +++ b/postfix/README_FILES/INSTALL @@ -376,27 +376,29 @@ whistles. Support for third-party databases etc. must be configured when Postfix is compiled. The following documents describe how to build Postfix with support for optional features: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - |OOppttiioonnaall ffeeaattuurree |DDooccuummeenntt |AAvvaaiillaabbiilliittyy| - |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | - |Berkeley DB database |DB_README |Postfix 1.0 | - |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | - |LMDB database |LMDB_README |Postfix 2.11| - |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | - |LDAP database |LDAP_README |Postfix 1.0 | - |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | - |MySQL database |MYSQL_README |Postfix 1.0 | - |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | - |Perl compatible regular expression|PCRE_README |Postfix 1.0 | - |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | - |PostgreSQL database |PGSQL_README |Postfix 2.0 | - |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | - |SASL authentication |SASL_README |Postfix 1.0 | - |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | - |SQLite database |SQLITE_README|Postfix 2.8 | - |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | - |STARTTLS session encryption |TLS_README |Postfix 2.2 | - |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + |OOppttiioonnaall ffeeaattuurree |DDooccuummeenntt |AAvvaaiillaabbiilliittyy| + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + |Berkeley DB database |DB_README |Postfix 1.0 | + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + |LMDB database |LMDB_README |Postfix 2.11| + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + |LDAP database |LDAP_README |Postfix 1.0 | + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + |MongoDB database |MONGODB_README|Postfix 3.9 | + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + |MySQL database |MYSQL_README |Postfix 1.0 | + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + |Perl compatible regular expression|PCRE_README |Postfix 1.0 | + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + |PostgreSQL database |PGSQL_README |Postfix 2.0 | + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + |SASL authentication |SASL_README |Postfix 1.0 | + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + |SQLite database |SQLITE_README |Postfix 2.8 | + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | + |STARTTLS session encryption |TLS_README |Postfix 2.2 | + |_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ _ _ _|_ _ _ _ _ _ _ _ _ _ _ _ | Note: IP version 6 support is compiled into Postfix on operating systems that have IPv6 support. See the IPV6_README file for details. diff --git a/postfix/README_FILES/MONGODB_README b/postfix/README_FILES/MONGODB_README new file mode 100644 index 000000000..aaeb71c07 --- /dev/null +++ b/postfix/README_FILES/MONGODB_README @@ -0,0 +1,169 @@ +PPoossttffiixx MMoonnggooDDBB HHoowwttoo + +------------------------------------------------------------------------------- + +MMoonnggooDDBB SSuuppppoorrtt iinn PPoossttffiixx + +Postfix can use MongoDB as a source for any of its lookups: aliases(5), virtual +(5), canonical(5), etc. This allows you to keep information for your mail +service in a replicated noSQL database with fine-grained access controls. By +not storing it locally on the mail server, the administrators can maintain it +from anywhere, and the users can control whatever bits of it you think +appropriate. You can have multiple mail servers using the same information, +without the hassle and delay of having to copy it to each. + +Topics covered in this document: + + * Building Postfix with MongoDB support + * Configuring MongoDB lookups + * Example: virtual alias maps + * Example: Mailing lists + * Example: MongoDB projections + * Feedback + +BBuuiillddiinngg PPoossttffiixx wwiitthh MMoonnggooDDBB ssuuppppoorrtt + +These instructions assume that you build Postfix from source code as described +in the INSTALL document. Some modification may be required if you build Postfix +from a vendor-specific source package. + +The Postfix MongoDB client requires the mongo-c-driver library. This can be +built from source code from the mongod-c project, or as a binary package from +your OS distribution, typically named mmoonnggoo--cc--ddrriivveerr--ddeevveell or lliibbmmoonnggoocc--ddeevv. +Installing the mongo-c-driver library may also install lliibbbbssoonn as a dependency. + +To build Postfix with mongodb map support, add to the CCARGS environment +variable the options -DHAS_MONGODB and -I for the directory containing the +mongodb headers, and specify the AUXLIBS_MONGODB with the libmongoc and libbson +libraries, for example: + + % make tidy + % make -f Makefile.init makefiles \ + CCARGS="$CCARGS -DHAS_MONGODB -I/usr/include/libmongoc-1.0 \ + -I/usr/include/libbson-1.0" \ + AUXLIBS_MONGODB="-lmongoc-1.0 -lbson-1.0" + +The 'make tidy' command is needed only if you have previously built Postfix +without MongoDB support. + +If your MongoDB shared library is in a directory that the RUN-TIME linker does +not know about, add a "-Wl,-R,/path/to/directory" option after "-lbson-1.0". +Then, just run 'make'. + +CCoonnffiigguurriinngg MMoonnggooDDBB llooookkuuppss + +In order to use MongoDB lookups, define a MongoDB source as a table lookup in +main.cf, for example: + + alias_maps = hash:/etc/aliases, proxy:mongodb:/etc/postfix/mongo-aliases.cf + +The file /etc/postfix/mongo-aliases.cf can specify a number of parameters. For +a complete description, see the mongodb_table(5) manual page. + +EExxaammppllee:: vviirrttuuaall((55)) aalliiaass mmaappss + +Here's a basic example for using MongoDB to look up virtual(5) aliases. Assume +that in main.cf, you have: + + virtual_alias_maps = hash:/etc/postfix/virtual_aliases, + proxy:mongodb:/etc/postfix/mongo-virtual-aliases.cf + +and in mongodb:/etc/postfix/mongo-virtual-aliases.cf you have: + + uri = mongodb+srv://user_name:password@some_server + dbname = mail + collection = mailbox + query_filter = {"$or": [{"username":"%s"}, {"alias.address": "%s"}], + "active": 1} + result_attribute = username + +This example assumes mailbox names are stored in a MongoDB backend, in a format +like: + + { "username": "user@example.com", + "alias": [ + {"address": "admin@example.com"}, + {"address": "abuse@example.com"} + ], + "active": 1 + } + +Upon receiving mail for "admin@example.com" that isn't found in the /etc/ +postfix/virtual_aliases database, Postfix will search the MongoDB server/ +cluster listening at port 27017 on some_server. It will connect using the +provided credentials, and search for any entries whose username is, or alias +field has "admin@example.com". It will return the username attribute of those +found, and build a list of their email addresses. + +EExxaammppllee:: MMaaiilliinngg lliissttss + +When it comes to mailing lists, one way of implementing one would be as below: + + { "name": "dev@example.com", "active": 1, "address": + [ "hamid@example.com", "wietse@example.com", "viktor@example.com" ] } + +using the filter below, will result in a comma separated string with all email +addresses in this list. + + query_filter = {"name": "%s", "active": 1} + result_attribute = address + +Notes: + + * As with pprroojjeeccttiioonn, the Postfix mongodb client automatically removes the + top-level '_id' field from a result. + + * The Postfix mongodb client will only parse result fields with data types + UTF8, INT32, INT64 and ARRAY. Other fields will be ignored, with a warning + in the logs. + +EExxaammppllee:: aaddvvaanncceedd pprroojjeeccttiioonnss + +This module also supports the use of more complex MongoDB projections. There +may be some use cases where operations such as concatenation are necessary to +be performed on the data retrieved from the database. Although it is encouraged +to keep the database design simple enough so this is not necessary, postfix +supports the use of MongoDB projections to achieve the goal. + +Consider the example below: + + { "username": "user@example.com", + "local_part": "user", + "domain": "example.com", + "alias": [ + {"address": "admin@example.com"}, + {"address": "abuse@example.com"} + ], + "active": 1 + } + +virtual_mailbox_maps can be created using below parameters in a mongodb:/etc/ +postfix/mongo-virtual-mailboxes.cf file: + + uri = mongodb+srv://user_name:password@some_server + dbname = mail + collection = mailbox + query_filter = {"$or": [{"username":"%s"}, {"alias.address": "%s"}], + "active": 1} + projection = { "mail_path": {"$concat": ["$domain", "/", "$local_part"]} } + +This will return 'example.com/user' path built from the database fields. + +A couple of considerations when using projections: + + * As with rreessuulltt__aattttrriibbuuttee, the Postfix mongodb client automatically removes + the top-level '_id' field from a projection result. + + * The Postfix mongodb client will only parse fields with data types UTF8, + INT32, INT64 and ARRAY. Other fields will be ignored, with a warning in the + logs. It is suggested to exclude any unnecessary fields when using a + projection. + +FFeeeeddbbaacckk + +If you have questions, send them to postfix-users@postfix.org. Please include +relevant information about your Postfix setup: MongoDB-related output from +postconf, which libraries you built with, and such. If your question involves +your database contents, please include the applicable bits of some database +entries. + diff --git a/postfix/conf/dynamicmaps.cf b/postfix/conf/dynamicmaps.cf index 5179f66f0..feeb6a11e 100644 --- a/postfix/conf/dynamicmaps.cf +++ b/postfix/conf/dynamicmaps.cf @@ -2,6 +2,7 @@ cdb ${LIB_PREFIX}cdb${LIB_SUFFIX} dict_cdb_open mkmap_cdb_open ldap ${LIB_PREFIX}ldap${LIB_SUFFIX} dict_ldap_open lmdb ${LIB_PREFIX}lmdb${LIB_SUFFIX} dict_lmdb_open mkmap_lmdb_open +mongodb ${LIB_PREFIX}mongodb${LIB_SUFFIX} dict_mongodb_open mysql ${LIB_PREFIX}mysql${LIB_SUFFIX} dict_mysql_open pcre ${LIB_PREFIX}pcre${LIB_SUFFIX} dict_pcre_open pgsql ${LIB_PREFIX}pgsql${LIB_SUFFIX} dict_pgsql_open diff --git a/postfix/conf/postfix-files b/postfix/conf/postfix-files index 12a7ccbc5..bbc4dcd21 100644 --- a/postfix/conf/postfix-files +++ b/postfix/conf/postfix-files @@ -75,6 +75,7 @@ $shlib_directory/lib${LIB_PREFIX}master${LIB_SUFFIX}:f:root:-:755 $shlib_directory/${LIB_PREFIX}cdb${LIB_SUFFIX}:f:root:-:755 $shlib_directory/${LIB_PREFIX}ldap${LIB_SUFFIX}:f:root:-:755 $shlib_directory/${LIB_PREFIX}lmdb${LIB_SUFFIX}:f:root:-:755 +$shlib_directory/${LIB_PREFIX}mongodb${LIB_SUFFIX}:f:root:-:755 $shlib_directory/${LIB_PREFIX}mysql${LIB_SUFFIX}:f:root:-:755 $shlib_directory/${LIB_PREFIX}pcre${LIB_SUFFIX}:f:root:-:755 $shlib_directory/${LIB_PREFIX}pgsql${LIB_SUFFIX}:f:root:-:755 @@ -194,6 +195,7 @@ $manpage_directory/man5/ldap_table.5:f:root:-:644 $manpage_directory/man5/lmdb_table.5:f:root:-:644 $manpage_directory/man5/master.5:f:root:-:644 $manpage_directory/man5/memcache_table.5:f:root:-:644 +$manpage_directory/man5/mongodb_table.5:f:root:-:644 $manpage_directory/man5/mysql_table.5:f:root:-:644 $manpage_directory/man5/socketmap_table.5:f:root:-:644 $manpage_directory/man5/sqlite_table.5:f:root:-:644 @@ -301,6 +303,7 @@ $readme_directory/MAILDROP_README:f:root:-:644 $readme_directory/MAILLOG_README:f:root:-:644 $readme_directory/MEMCACHE_README:f:root:-:644 $readme_directory/MILTER_README:f:root:-:644 +$readme_directory/MONGODB_README:f:root:-:644 $readme_directory/MULTI_INSTANCE_README:f:root:-:644 $readme_directory/MYSQL_README:f:root:-:644 $readme_directory/SMTPUTF8_README:f:root:-:644 @@ -362,6 +365,7 @@ $html_directory/MAILDROP_README.html:f:root:-:644 $html_directory/MAILLOG_README.html:f:root:-:644 $html_directory/MEMCACHE_README.html:f:root:-:644 $html_directory/MILTER_README.html:f:root:-:644 +$html_directory/MONGODB_README.html:f:root:-:644 $html_directory/MULTI_INSTANCE_README.html:f:root:-:644 $html_directory/MYSQL_README.html:f:root:-:644 $html_directory/SMTPUTF8_README.html:f:root:-:644 @@ -418,6 +422,7 @@ $html_directory/mailq.1.html:f:root:-:644 $html_directory/master.5.html:f:root:-:644 $html_directory/master.8.html:f:root:-:644 $html_directory/memcache_table.5.html:f:root:-:644 +$html_directory/mongodb_table.5.html:f:root:-:644 $html_directory/mysql_table.5.html:f:root:-:644 $html_directory/sqlite_table.5.html:f:root:-:644 $html_directory/nisplus_table.5.html:f:root:-:644 diff --git a/postfix/html/DATABASE_README.html b/postfix/html/DATABASE_README.html index 1f7b183c3..0e3e22287 100644 --- a/postfix/html/DATABASE_README.html +++ b/postfix/html/DATABASE_README.html @@ -349,6 +349,11 @@ See lmdb_table(5) for details.
Memcache database client. Configuration details are given in memcache_table(5).
+
mongodb (read-only)
+ +
MongoDB database client. Configuration details are given in +mongodb_table(5), with examples in MONGODB_README.
+
mysql (read-only)
MySQL database client. Configuration details are given in diff --git a/postfix/html/INSTALL.html b/postfix/html/INSTALL.html index 6cd70d113..94d78baa7 100644 --- a/postfix/html/INSTALL.html +++ b/postfix/html/INSTALL.html @@ -605,6 +605,9 @@ describe how to build Postfix with support for optional features: LDAP database LDAP_README Postfix 1.0 + MongoDB database MONGODB_README Postfix +3.9 + MySQL database MYSQL_README Postfix 1.0 diff --git a/postfix/html/MONGODB_README.html b/postfix/html/MONGODB_README.html new file mode 100644 index 000000000..278c0c813 --- /dev/null +++ b/postfix/html/MONGODB_README.html @@ -0,0 +1,232 @@ + + + +Postfix MongoDB Howto + + + +

Postfix MongoDB Howto

+
+ +

MongoDB Support in Postfix

+ +

Postfix can use MongoDB as a source for any of its lookups: +aliases(5), virtual(5), canonical(5), etc. This allows you to keep +information for your mail service in a replicated noSQL database +with fine-grained access controls. By not storing it locally on the +mail server, the administrators can maintain it from anywhere, and +the users can control whatever bits of it you think appropriate. +You can have multiple mail servers using the same information, +without the hassle and delay of having to copy it to each.

+ +

Topics covered in this document:

+ + + +

Building Postfix with MongoDB support

+ +

These instructions assume that you build Postfix from source +code as described in the INSTALL document. Some modification may +be required if you build Postfix from a vendor-specific source +package.

+ +

The Postfix MongoDB client requires the mongo-c-driver library. +This can be built from source code from the +mongod-c project, or as a binary package from your OS distribution, +typically named mongo-c-driver-devel or libmongoc-dev. +Installing the mongo-c-driver library may also install libbson +as a dependency.

+ +

To build Postfix with mongodb map support, add to the CCARGS +environment variable the options -DHAS_MONGODB and -I for the +directory containing the mongodb headers, and specify the AUXLIBS_MONGODB +with the libmongoc and libbson libraries, for example:

+ +
+
+% make tidy
+% make -f Makefile.init makefiles \
+    CCARGS="$CCARGS -DHAS_MONGODB -I/usr/include/libmongoc-1.0 \
+    -I/usr/include/libbson-1.0" \
+    AUXLIBS_MONGODB="-lmongoc-1.0 -lbson-1.0"
+
+
+ +

The 'make tidy' command is needed only if you have previously +built Postfix without MongoDB support.

+ +

If your MongoDB shared library is in a directory that the RUN-TIME +linker does not know about, add a "-Wl,-R,/path/to/directory" option +after "-lbson-1.0". Then, just run 'make'.

+ +

Configuring MongoDB lookups

+ +

In order to use MongoDB lookups, define a MongoDB source as a +table lookup in main.cf, for example:

+ +
+
+alias_maps = hash:/etc/aliases, proxy:mongodb:/etc/postfix/mongo-aliases.cf
+
+
+ +

The file /etc/postfix/mongo-aliases.cf can specify a number of +parameters. For a complete description, see the mongodb_table(5) +manual page.

+ +

Example: virtual(5) alias maps

+ +

Here's a basic example for using MongoDB to look up virtual(5) +aliases. Assume that in main.cf, you have:

+ +
+
+virtual_alias_maps = hash:/etc/postfix/virtual_aliases, 
+    proxy:mongodb:/etc/postfix/mongo-virtual-aliases.cf
+
+
+ +

and in mongodb:/etc/postfix/mongo-virtual-aliases.cf you have:

+ +
+
+uri = mongodb+srv://user_name:password@some_server
+dbname = mail
+collection = mailbox
+query_filter = {"$or": [{"username":"%s"}, {"alias.address": "%s"}], "active": 1}
+result_attribute = username
+
+
+ +

This example assumes mailbox names are stored in a MongoDB backend, +in a format like:

+ +
+
+{ "username": "user@example.com",
+  "alias": [
+    {"address": "admin@example.com"},
+    {"address": "abuse@example.com"}
+  ],
+  "active": 1
+}
+
+
+ +

Upon receiving mail for "admin@example.com" that isn't found in the +/etc/postfix/virtual_aliases database, Postfix will search the +MongoDB server/cluster listening at port 27017 on some_server. It +will connect using the provided credentials, and search for any +entries whose username is, or alias field has "admin@example.com". +It will return the username attribute of those found, and build a +list of their email addresses.

+ +

Example: Mailing lists

+ +

When it comes to mailing lists, one way of implementing one would +be as below:

+ +
+
+{ "name": "dev@example.com", "active": 1, "address": 
+  [ "hamid@example.com", "wietse@example.com", "viktor@example.com" ] }
+
+
+ +

using the filter below, will result in a comma separated string +with all email addresses in this list.

+ +
+
+query_filter = {"name": "%s", "active": 1}
+result_attribute = address
+
+
+ +

Notes:

+ + + +

Example: advanced projections

+ +

This module also supports the use of more complex MongoDB +projections. There may be some use cases where operations such as +concatenation are necessary to be performed on the data retrieved +from the database. Although it is encouraged to keep the database +design simple enough so this is not necessary, postfix supports the +use of MongoDB projections to achieve the goal.

+ +

Consider the example below:

+ +
+
+{ "username": "user@example.com",
+  "local_part": "user",
+  "domain": "example.com",
+  "alias": [
+    {"address": "admin@example.com"},
+    {"address": "abuse@example.com"}
+  ],
+  "active": 1
+}
+
+
+ +

virtual_mailbox_maps can be created using below parameters in a +mongodb:/etc/postfix/mongo-virtual-mailboxes.cf file:

+ +
+
+uri = mongodb+srv://user_name:password@some_server
+dbname = mail
+collection = mailbox
+query_filter = {"$or": [{"username":"%s"}, {"alias.address": "%s"}], "active": 1}
+projection = { "mail_path": {"$concat": ["$domain", "/", "$local_part"]} }
+
+
+ +

This will return 'example.com/user' path built from the database fields.

+ +

A couple of considerations when using projections:

+ + +

Feedback

+ +

If you have questions, send them to postfix-users@postfix.org. +Please include relevant information about your Postfix setup: +MongoDB-related output from postconf, which libraries you built +with, and such. If your question involves your database contents, +please include the applicable bits of some database entries.

+ + + + diff --git a/postfix/html/Makefile.in b/postfix/html/Makefile.in index c5481f8af..7f23ed7fd 100644 --- a/postfix/html/Makefile.in +++ b/postfix/html/Makefile.in @@ -20,7 +20,7 @@ CONFIG = access.5.html aliases.5.html canonical.5.html relocated.5.html \ transport.5.html virtual.5.html pcre_table.5.html regexp_table.5.html \ cidr_table.5.html tcp_table.5.html header_checks.5.html \ ldap_table.5.html lmdb_table.5.html mysql_table.5.html \ - pgsql_table.5.html memcache_table.5.html \ + pgsql_table.5.html memcache_table.5.html mongodb_table.5.html \ master.5.html nisplus_table.5.html generic.5.html bounce.5.html \ postfix-wrapper.5.html sqlite_table.5.html socketmap_table.5.html OTHER = postfix-manuals.html @@ -298,6 +298,10 @@ memcache_table.5.html: ../proto/memcache_table PATH=../mantools:$$PATH; \ srctoman - $? | $(AWK) | $(NROFF) -man | uniq | $(MAN2HTML) | postlink >$@ +mongodb_table.5.html: ../proto/mongodb_table + PATH=../mantools:$$PATH; \ + srctoman - $? | $(AWK) | $(NROFF) -man | uniq | $(MAN2HTML) | postlink >$@ + mysql_table.5.html: ../proto/mysql_table PATH=../mantools:$$PATH; \ srctoman - $? | $(AWK) | $(NROFF) -man | uniq | $(MAN2HTML) | postlink >$@ diff --git a/postfix/html/index.html b/postfix/html/index.html index 68edc59b3..fe1bfab98 100644 --- a/postfix/html/index.html +++ b/postfix/html/index.html @@ -141,6 +141,8 @@ Per-client/user/etc. access
  • Memcache Howto +
  • MongoDB Howto +
  • MySQL Howto
  • PCRE Howto diff --git a/postfix/html/makedefs.1.html b/postfix/html/makedefs.1.html index 7202a8df9..ce6725713 100644 --- a/postfix/html/makedefs.1.html +++ b/postfix/html/makedefs.1.html @@ -34,9 +34,9 @@ MAKEDEFS(1) MAKEDEFS(1) AUXLIBS=object_library... Specifies one or more non-default object libraries. Postfix 3.0 and later specify some of their database library dependencies - with AUXLIBS_CDB, AUXLIBS_LDAP, AUXLIBS_LMDB, AUXLIBS_MYSQL, - AUXLIBS_PCRE, AUXLIBS_PGSQL, AUXLIBS_SDBM, and AUXLIBS_SQLITE, - respectively. + with AUXLIBS_CDB, AUXLIBS_LDAP, AUXLIBS_LMDB, AUXLIBS_MONGODB, + AUXLIBS_MYSQL, AUXLIBS_PCRE, AUXLIBS_PGSQL, AUXLIBS_SDBM, and + AUXLIBS_SQLITE, respectively. CC=compiler_command Specifies a non-default compiler. On many systems, the default diff --git a/postfix/html/mongodb_table.5.html b/postfix/html/mongodb_table.5.html new file mode 100644 index 000000000..b7434f2f9 --- /dev/null +++ b/postfix/html/mongodb_table.5.html @@ -0,0 +1,215 @@ + + + + + Postfix manual - mongodb_table(5) +
    +MONGODB_TABLE(5)                                              MONGODB_TABLE(5)
    +
    +NAME
    +       mongodb_table - Postfix MongoDB client configuration
    +
    +SYNOPSIS
    +       postmap -q "string" mongodb:/etc/postfix/filename
    +
    +       postmap -q - mongodb:/etc/postfix/filename <inputfile
    +
    +DESCRIPTION
    +       The  Postfix  mail system uses optional tables for address rewriting or
    +       mail routing. These tables are usually in dbm or db format.
    +
    +       Alternatively, lookup tables can be specified as MongoDB databases.  In
    +       order to use MongoDB lookups, define a MongoDB source as a lookup table
    +       in main.cf, for example:
    +           alias_maps = mongodb:/etc/postfix/mongodb-aliases.cf
    +
    +       In this example, the file /etc/postfix/mongodb-aliases.cf has the  same
    +       format  as  the  Postfix  main.cf  file, and can specify the parameters
    +       described below. It is also  possible  to  have  the  configuration  in
    +       main.cf; see "OBSOLETE MAIN.CF PARAMETERS" below.
    +
    +       It is strongly recommended to use proxy:mongodb, in order to reduce the
    +       number of database connections. For example:
    +           alias_maps = proxy:mongodb:/etc/postfix/mongodb-aliases.cf
    +
    +       Note: when using proxy:mongodb:/file, the file must be readable by  the
    +       unprivileged  postfix  user (specified with the Postfix mail_owner con-
    +       figuration parameter).
    +
    +MONGODB PARAMETERS
    +       uri    The URI of mongo server/cluster that Postfix will try to connect
    +              to and query from. Please see
    +              https://www.mongodb.com/docs/manual/reference/connection-string/
    +
    +              Example:
    +                  uri = mongodb+srv://user:pass@loclhost:27017/mail
    +
    +       dbname Name of the database to read the information from.  Example:
    +                  dbname = mail
    +
    +       collection
    +              Name  of  the  collection  (table) to read the information from.
    +              Example:
    +                  collection = mailbox
    +
    +       query_filter
    +              The MongoDB query template used to search the database, where %s
    +              is  a substitute for the email address that Postfix is trying to
    +              resolve. Please see:
    +              https://www.mongodb.com/docs/manual/tutorial/query-documents/
    +
    +              Example:
    +                  query_filter = {"$or": [{"username": "%s"}, {"alias.address": "%s"}], "active": 1}
    +
    +              This parameter supports the following '%' expansions:
    +
    +              %%     This is replaced by a literal '%' character.
    +
    +              %s     This is replaced by the input key. The %s must appear  in
    +                     quotes,  because all Postfix queries are strings contain-
    +                     ing (parts from) a domain or email address. Postfix makes
    +                     no numerical queries.
    +
    +              %u     When the input key is an address of the form user@domain,
    +                     %u is replaced by the local part of the address.   Other-
    +                     wise, %u is replaced by the entire search string.
    +
    +              %d     When the input key is an address of the form user@domain,
    +                     %d is replaced by the domain part of the address.
    +
    +              %[1-9] The patterns %1, %2, ... %9 are replaced  by  the  corre-
    +                     sponding  most  significant  component of the input key's
    +                     domain. If the input key is  user@mail.example.com,  then
    +                     %1 is com, %2 is example and %3 is mail.
    +
    +              In  the  above  substitutions,  characters  will  be  quoted  as
    +              required by RFC 4627. For example, each double  quote  or  back-
    +              slash character will be escaped with a backslash characacter.
    +
    +       projection
    +              Advanced MongoDB query projections. Please see:
    +              https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/
    +
    +              o      If projection is non-empty, then result_attribute must be
    +                     empty.
    +
    +              o      This implementation can  extract  information  only  from
    +                     result  fields  that  have  type  string  (UTF8), integer
    +                     (int32, int64) and array. Other  result  fields  will  be
    +                     ignored with a warning. Please see:
    +                     https://mongoc.org/libbson/current/bson_type_t.html
    +
    +              o      As  with  result_attribute, the top-level _id field (type
    +                     OID) is automatically removed from projection results.
    +
    +       result_attribute
    +              Comma or whitespace separated list with the names of  fields  to
    +              be returned in a lookup result.
    +
    +              o      If result_attribute is non-empty, then projection must be
    +                     empty.
    +
    +              o      As with projection, the top-level _id field (type OID) is
    +                     automatically removed from lookup results.
    +
    +       result_format (default: %s)
    +              Format  template  applied  to  the  result  from  projection  or
    +              result_attribute. Most commonly used to append (or prepend) text
    +              to  the result. This parameter supports the following '%' expan-
    +              sions:
    +
    +              %%     This is replaced by a literal '%' character.
    +
    +              %s     This is replaced by the value of  the  result  attribute.
    +                     When result is empty it is skipped.
    +
    +              %u     When the result attribute value is an address of the form
    +                     user@domain, %u is replaced by  the  local  part  of  the
    +                     address.  When  the  result  has an empty localpart it is
    +                     skipped.
    +
    +              %d     When a result attribute value is an address of  the  form
    +                     user@domain,  %d  is  replaced  by the domain part of the
    +                     attribute value. When the result  is  unqualified  it  is
    +                     skipped.
    +
    +              %[SUD1-9]
    +                     The  upper-case  and decimal digit expansions interpolate
    +                     the parts of the input key rather than the result.  Their
    +                     behavior  is  identical to that described with query_fil-
    +                     ter, and in fact  because  the  input  key  is  known  in
    +                     advance,  lookups  whose  key  does  not  contain all the
    +                     information specified in the  result  template  are  sup-
    +                     pressed and return no results.
    +
    +              For example, using "result_format = smtp:[%s]" allows one to use
    +              a mailHost attribute as the basis of a transport(5) table. After
    +              applying  the result format, multiple values are concatenated as
    +              comma separated strings. The expansion_limit parameter explained
    +              below allows one to restrict the number of values in the result,
    +              which is especially useful for maps that should return a  single
    +              value.
    +
    +              The  default value %s specifies that each attribute value should
    +              be used as is.
    +
    +              NOTE: DO NOT put quotes around the result format! The result  is
    +              not a JSON string.
    +
    +       domain (default: no domain list)
    +              This  is a list of domain names, paths to files, or "type:table"
    +              databases. When specified, only fully qualified search keys with
    +              a  *non-empty*  localpart and a matching domain are eligible for
    +              lookup:  'user'  lookups,  bare  domain  lookups  and  "@domain"
    +              lookups  are  not  performed.  This can significantly reduce the
    +              query load on the backend database. Example:
    +                  domain = postfix.org, hash:/etc/postfix/searchdomains
    +
    +       expansion_limit (default: 0)
    +              A limit on the total number of result elements  returned  (as  a
    +              comma separated list) by a lookup against the map.  A setting of
    +              zero disables the limit. Lookups fail with a temporary error  if
    +              the  limit  is  exceeded.  Setting  the  limit to 1 ensures that
    +              lookups do not return multiple values.
    +
    +OBSOLETE MAIN.CF PARAMETERS
    +       MongoDB parameters can also be defined in main.cf. Specify  as  MongoDB
    +       source  a  name  that  doesn't begin with a slash or a dot. The MongoDB
    +       parameters will then be accessible as the name you've given the  source
    +       in  its  definition,  an underscore, and the name of the parameter. For
    +       example, if a map is specified as "mongodb:mongodb_source",  the  "uri"
    +       parameter would be defined in main.cf as "mongodb_source_uri".
    +
    +       Note:  with  this form, passwords are written in main.cf, which is nor-
    +       mally world-readable, and '$' in a mongodb parameter setting  needs  to
    +       be written as '$$'.
    +
    +SEE ALSO
    +       postmap(1), Postfix lookup table maintenance
    +       postconf(5), configuration parameters
    +
    +README FILES
    +       DATABASE_README, Postfix lookup table overview
    +       MONGODB_README, Postfix MONGODB client guide
    +
    +LICENSE
    +       The Secure Mailer license must be distributed with this software.
    +
    +HISTORY
    +       MongoDB support was introduced with Postfix version 3.9.
    +
    +AUTHOR(S)
    +       Hamid Maadani (hamid@dexo.tech)
    +       Dextrous Technologies, LLC
    +
    +       Edited by:
    +       Wietse Venema
    +       porcupine.org
    +
    +       Based on prior work by:
    +       Stephan Ferraro
    +       Aionda GmbH
    +
    +                                                              MONGODB_TABLE(5)
    +
    diff --git a/postfix/html/postfix-manuals.html b/postfix/html/postfix-manuals.html index 936ecae8f..84774a52b 100644 --- a/postfix/html/postfix-manuals.html +++ b/postfix/html/postfix-manuals.html @@ -164,6 +164,8 @@ the following convention:

  • memcache_table(5), Postfix memcache client +
  • mongodb_table(5), Postfix MongoDB client +
  • mysql_table(5), Postfix MYSQL client
  • nisplus_table(5), Postfix NIS+ client diff --git a/postfix/html/postfix.1.html b/postfix/html/postfix.1.html index 90751ab55..a6ede786f 100644 --- a/postfix/html/postfix.1.html +++ b/postfix/html/postfix.1.html @@ -359,6 +359,7 @@ POSTFIX(1) POSTFIX(1) ldap_table(5), Postfix LDAP client lmdb_table(5), Postfix LMDB database driver memcache_table(5), Postfix memcache client + mongodb_table(5), Postfix MongoDB client mysql_table(5), Postfix MYSQL client nisplus_table(5), Postfix NIS+ client pcre_table(5), Associate PCRE pattern with value diff --git a/postfix/makedefs b/postfix/makedefs index 98c826735..430324d4e 100644 --- a/postfix/makedefs +++ b/postfix/makedefs @@ -35,6 +35,7 @@ # Specifies one or more non-default object libraries. Postfix # 3.0 and later specify some of their database library # dependencies with AUXLIBS_CDB, AUXLIBS_LDAP, AUXLIBS_LMDB, +# AUXLIBS_MONGODB, # AUXLIBS_MYSQL, AUXLIBS_PCRE, AUXLIBS_PGSQL, AUXLIBS_SDBM, # and AUXLIBS_SQLITE, respectively. # .IP \fBCC=\fIcompiler_command\fR @@ -1245,7 +1246,7 @@ DEFINED_MAP_TYPES=` # Propagate AUXLIBS_FOO or merge them into global AUXLIBS (i.e. SYSLIBS). -PLUGGABLE_MAPS="CDB LDAP LMDB MYSQL PCRE PGSQL SDBM SQLITE" +PLUGGABLE_MAPS="CDB LDAP LMDB MONGODB MYSQL PCRE PGSQL SDBM SQLITE" case "$dynamicmaps" in yes) for name in $PLUGGABLE_MAPS diff --git a/postfix/man/Makefile.in b/postfix/man/Makefile.in index f98402ca9..40a52363c 100644 --- a/postfix/man/Makefile.in +++ b/postfix/man/Makefile.in @@ -17,7 +17,7 @@ CONFIG = man5/access.5 man5/aliases.5 man5/canonical.5 man5/relocated.5 \ man5/transport.5 man5/virtual.5 man5/pcre_table.5 man5/regexp_table.5 \ man5/cidr_table.5 man5/tcp_table.5 man5/header_checks.5 \ man5/body_checks.5 man5/ldap_table.5 man5/lmdb_table.5 \ - man5/memcache_table.5 man5/mysql_table.5 \ + man5/memcache_table.5 man5/mongodb_table.5 man5/mysql_table.5 \ man5/pgsql_table.5 man5/master.5 man5/nisplus_table.5 \ man5/generic.5 man5/bounce.5 man5/postfix-wrapper.5 \ man5/sqlite_table.5 man5/socketmap_table.5 @@ -316,6 +316,11 @@ man5/memcache_table.5: ../proto/memcache_table (cmp -s junk $? || mv junk $?) && rm -f junk ../mantools/srctoman - $? >$@ +man5/mongodb_table.5: ../proto/mongodb_table + ../mantools/fixman ../proto/postconf.proto $? >junk && \ + (cmp -s junk $? || mv junk $?) && rm -f junk + ../mantools/srctoman - $? >$@ + man5/mysql_table.5: ../proto/mysql_table ../mantools/fixman ../proto/postconf.proto $? >junk && \ (cmp -s junk $? || mv junk $?) && rm -f junk diff --git a/postfix/man/man1/makedefs.1 b/postfix/man/man1/makedefs.1 index 70c848e0d..c921ac258 100644 --- a/postfix/man/man1/makedefs.1 +++ b/postfix/man/man1/makedefs.1 @@ -38,6 +38,7 @@ of the make(1) command. Specifies one or more non\-default object libraries. Postfix 3.0 and later specify some of their database library dependencies with AUXLIBS_CDB, AUXLIBS_LDAP, AUXLIBS_LMDB, +AUXLIBS_MONGODB, AUXLIBS_MYSQL, AUXLIBS_PCRE, AUXLIBS_PGSQL, AUXLIBS_SDBM, and AUXLIBS_SQLITE, respectively. .IP \fBCC=\fIcompiler_command\fR diff --git a/postfix/man/man1/postfix.1 b/postfix/man/man1/postfix.1 index fc690bed7..da39dc5ca 100644 --- a/postfix/man/man1/postfix.1 +++ b/postfix/man/man1/postfix.1 @@ -330,6 +330,7 @@ cidr_table(5), Associate CIDR pattern with value ldap_table(5), Postfix LDAP client lmdb_table(5), Postfix LMDB database driver memcache_table(5), Postfix memcache client +mongodb_table(5), Postfix MongoDB client mysql_table(5), Postfix MYSQL client nisplus_table(5), Postfix NIS+ client pcre_table(5), Associate PCRE pattern with value diff --git a/postfix/man/man5/mongodb_table.5 b/postfix/man/man5/mongodb_table.5 new file mode 100644 index 000000000..cfbedf37a --- /dev/null +++ b/postfix/man/man5/mongodb_table.5 @@ -0,0 +1,259 @@ +.TH MONGODB_TABLE 5 +.ad +.fi +.SH NAME +mongodb_table +\- +Postfix MongoDB client configuration +.SH "SYNOPSIS" +.na +.nf +\fBpostmap \-q "\fIstring\fB" mongodb:/etc/postfix/\fIfilename\fR + +\fBpostmap \-q \- mongodb:/etc/postfix/\fIfilename\fB <\fIinputfile\fR +.SH DESCRIPTION +.ad +.fi +The Postfix mail system uses optional tables for address +rewriting or mail routing. These tables are usually in +\fBdbm\fR or \fBdb\fR format. + +Alternatively, lookup tables can be specified as MongoDB +databases. In order to use MongoDB lookups, define a MongoDB +source as a lookup table in main.cf, for example: +.nf + alias_maps = mongodb:/etc/postfix/mongodb\-aliases.cf +.fi + +In this example, the file /etc/postfix/mongodb\-aliases.cf +has the same format as the Postfix main.cf file, and can +specify the parameters described below. It is also possible +to have the configuration in main.cf; see "OBSOLETE MAIN.CF +PARAMETERS" below. + +It is strongly recommended to use proxy:mongodb, in order +to reduce the number of database connections. For example: +.nf + alias_maps = proxy:mongodb:/etc/postfix/mongodb\-aliases.cf +.fi + +Note: when using proxy:mongodb:/\fIfile\fR, the file must +be readable by the unprivileged postfix user (specified +with the Postfix mail_owner configuration parameter). +.SH "MONGODB PARAMETERS" +.na +.nf +.ad +.fi +.IP "\fBuri\fR" +The URI of mongo server/cluster that Postfix will try to +connect to and query from. Please see +.nf +https://www.mongodb.com/docs/manual/reference/connection\-string/ +.fi + +Example: +.nf + uri = mongodb+srv://user:pass@loclhost:27017/mail +.fi +.IP "\fBdbname\fR" +Name of the database to read the information from. +Example: +.nf + dbname = mail +.fi +.IP "\fBcollection\fR" +Name of the collection (table) to read the information from. +Example: +.nf + collection = mailbox +.fi +.IP "\fBquery_filter\fR" +The MongoDB query template used to search the database, +where \fB%s\fR is a substitute for the email address that +Postfix is trying to resolve. Please see: +.nf +https://www.mongodb.com/docs/manual/tutorial/query\-documents/ +.fi + +Example: +.nf + query_filter = {"$or": [{"username": "%s"}, {"alias.address": "%s"}], "active": 1} +.fi + +This parameter supports the following '%' expansions: +.RS +.IP "\fB%%\fR" +This is replaced by a literal '%' character. +.IP "\fB%s\fR" +This is replaced by the input key. The %s must appear in +quotes, because all Postfix queries are strings containing +(parts from) a domain or email address. Postfix makes no +numerical queries. +.IP "\fB%u\fR" +When the input key is an address of the form user@domain, +\fB%u\fR is replaced by the local part of the address. +Otherwise, \fB%u\fR is replaced by the entire search string. +.IP "\fB%d\fR" +When the input key is an address of the form user@domain, +\fB%d\fR is replaced by the domain part of the address. +.IP "\fB%[1\-9]\fR" +The patterns %1, %2, ... %9 are replaced by the corresponding +most significant component of the input key's domain. If +the input key is \fIuser@mail.example.com\fR, then %1 is +\fBcom\fR, %2 is \fBexample\fR and %3 is \fBmail\fR. +.RE +.IP +In the above substitutions, characters will be quoted as +required by RFC 4627. For example, each double quote or +backslash character will be escaped with a backslash +characacter. +.IP "\fBprojection\fR" +Advanced MongoDB query projections. Please see: +.nf +https://www.mongodb.com/docs/manual/tutorial/project\-fields\-from\-query\-results/ +.fi + +.RS +.IP \(bu +If \fBprojection\fR is non\-empty, then \fBresult_attribute\fR +must be empty. +.IP \(bu +This implementation can extract information only from result +fields that have type \fBstring\fR (UTF8), \fBinteger\fR +(int32, int64) and \fBarray\fR. Other result fields will +be ignored with a warning. Please see: +.nf +https://mongoc.org/libbson/current/bson_type_t.html +.fi +.IP \(bu +As with \fBresult_attribute\fR, the top\-level _id field +(type OID) is automatically removed from projection results. +.RE +.IP "\fBresult_attribute\fR" +Comma or whitespace separated list with the names of fields +to be returned in a lookup result. + +.RS +.IP \(bu +If \fBresult_attribute\fR is non\-empty, then \fBprojection\fR +must be empty. +.IP \(bu +As with \fBprojection\fR, the top\-level _id field (type +OID) is automatically removed from lookup results. +.RE +.IP "\fBresult_format (default: \fB%s\fR)\fR" +Format template applied to the result from \fBprojection\fR +or \fBresult_attribute\fR. Most commonly used to append (or +prepend) text to the result. This parameter supports the +following '%' expansions: +.RS +.IP "\fB%%\fR" +This is replaced by a literal '%' character. +.IP "\fB%s\fR" +This is replaced by the value of the result attribute. When +result is empty it is skipped. +.IP "\fB%u\fR +When the result attribute value is an address of the form +user@domain, \fB%u\fR is replaced by the local part of the +address. When the result has an empty localpart it is +skipped. +.IP "\fB%d\fR" +When a result attribute value is an address of the form +user@domain, \fB%d\fR is replaced by the domain part of the +attribute value. When the result is unqualified it is +skipped. +.IP "\fB%[SUD1\-9]\fR" +The upper\-case and decimal digit expansions interpolate the +parts of the input key rather than the result. Their behavior +is identical to that described with \fBquery_filter\fR, and +in fact because the input key is known in advance, lookups +whose key does not contain all the information specified +in the result template are suppressed and return no results. +.RE +.IP +For example, using "result_format = smtp:[%s]" allows one +to use a mailHost attribute as the basis of a transport(5) +table. After applying the result format, multiple values +are concatenated as comma separated strings. The expansion_limit +parameter explained below allows one to restrict the number +of values in the result, which is especially useful for +maps that should return a single value. + +The default value \fB%s\fR specifies that each +attribute value should be used as is. + +NOTE: DO NOT put quotes around the result format! The result +is not a JSON string. +.IP "\fBdomain (default: no domain list)\fR" +This is a list of domain names, paths to files, or "type:table" +databases. When specified, only fully qualified search keys +with a *non\-empty* localpart and a matching domain are +eligible for lookup: 'user' lookups, bare domain lookups +and "@domain" lookups are not performed. This can significantly +reduce the query load on the backend database. Example: +.nf + domain = postfix.org, hash:/etc/postfix/searchdomains +.fi +.IP "\fBexpansion_limit (default: 0)\fR" +A limit on the total number of result elements returned (as +a comma separated list) by a lookup against the map. A +setting of zero disables the limit. Lookups fail with a +temporary error if the limit is exceeded. Setting the limit +to 1 ensures that lookups do not return multiple values. +.SH "OBSOLETE MAIN.CF PARAMETERS" +.na +.nf +.ad +.fi +MongoDB parameters can also be defined in main.cf. Specify +as MongoDB source a name that doesn't begin with a slash +or a dot. The MongoDB parameters will then be accessible +as the name you've given the source in its definition, an +underscore, and the name of the parameter. For example, if +a map is specified as "mongodb:\fImongodb_source\fR", the +"uri" parameter would be defined in main.cf as +"\fImongodb_source\fR_uri". + +Note: with this form, passwords are written in main.cf, +which is normally world\-readable, and '$' in a mongodb +parameter setting needs to be written as '$$'. +.SH "SEE ALSO" +.na +.nf +postmap(1), Postfix lookup table maintenance +postconf(5), configuration parameters +.SH "README FILES" +.na +.nf +.ad +.fi +Use "\fBpostconf readme_directory\fR" or "\fBpostconf +html_directory\fR" to locate this information. +.na +.nf +DATABASE_README, Postfix lookup table overview +MONGODB_README, Postfix MONGODB client guide +.SH "LICENSE" +.na +.nf +.ad +.fi +The Secure Mailer license must be distributed with this software. +.SH HISTORY +.ad +.fi +MongoDB support was introduced with Postfix version 3.9. +.SH "AUTHOR(S)" +.na +.nf +Hamid Maadani (hamid@dexo.tech) +Dextrous Technologies, LLC + +Edited by: +Wietse Venema +porcupine.org + +Based on prior work by: +Stephan Ferraro +Aionda GmbH diff --git a/postfix/mantools/postlink b/postfix/mantools/postlink index 783836108..6ca24c63d 100755 --- a/postfix/mantools/postlink +++ b/postfix/mantools/postlink @@ -895,6 +895,7 @@ while (<>) { s/[]*lmdb[<\/bBiI>]*_[<\/iIbB>]*ta[-<\/bB>]*\n*[ ]*ble[<\/bB>]*\(5\)/$&<\/a>/g; s/[]*mas[-<\/bB>]*\n* *[]*ter[<\/bB>]*\(5\)/$&<\/a>/g; s/[]*mem[-<\/bB>]*\n* *[]*cache[<\/bBiI>]*_[<\/iIbB>]*ta[-<\/bB>]*\n*[ ]*ble[<\/bB>]*\(5\)/$&<\/a>/g; + s/[]*mongodb[<\/bBiI>]*_[<\/iIbB>]*ta[-<\/bB>]*\n*[ ]*ble[<\/bB>]*\(5\)/$&<\/a>/g; s/[]*mysql[<\/bBiI>]*_[<\/iIbB>]*ta[-<\/bB>]*\n*[ ]*ble[<\/bB>]*\(5\)/$&<\/a>/g; s/[]*nisplus[<\/bBiI>]*_[<\/iIbB>]*ta[-<\/bB>]*\n*[ ]*ble[<\/bB>]*\(5\)/$&<\/a>/g; s/[]*pcre[<\/bBiI>]*_[<\/iIbB>]*ta[-<\/bB>]*\n*[ ]*ble[<\/bB>]*\(5\)/$&<\/a>/g; @@ -1252,6 +1253,7 @@ while (<>) { s/\b(ldap[is]?):/$1<\/a>:/g; s/\b(lmdb):/$1<\/a>:/g; s/\b(memcache):/$1<\/a>:/g; + s/\b(mongodb):/$1<\/a>:/g; s/\b(mysql):/$1<\/a>:/g; s/\b(nisplus):/$1<\/a>:/g; s/\b(pcre):/$1<\/a>:/g; diff --git a/postfix/proto/DATABASE_README.html b/postfix/proto/DATABASE_README.html index cb25f35bb..e9d0c189c 100644 --- a/postfix/proto/DATABASE_README.html +++ b/postfix/proto/DATABASE_README.html @@ -349,6 +349,11 @@ ldap_table(5).
  • Memcache database client. Configuration details are given in memcache_table(5).
    +
    mongodb (read-only)
    + +
    MongoDB database client. Configuration details are given in +mongodb_table(5), with examples in MONGODB_README.
    +
    mysql (read-only)
    MySQL database client. Configuration details are given in diff --git a/postfix/proto/INSTALL.html b/postfix/proto/INSTALL.html index 4686f030e..50249b28d 100644 --- a/postfix/proto/INSTALL.html +++ b/postfix/proto/INSTALL.html @@ -605,6 +605,9 @@ describe how to build Postfix with support for optional features: LDAP database LDAP_README Postfix 1.0 + MongoDB database MONGODB_README Postfix +3.9 + MySQL database MYSQL_README Postfix 1.0 diff --git a/postfix/proto/MONGODB_README.html b/postfix/proto/MONGODB_README.html new file mode 100644 index 000000000..c660fc199 --- /dev/null +++ b/postfix/proto/MONGODB_README.html @@ -0,0 +1,232 @@ + + + +Postfix MongoDB Howto + + + +

    Postfix MongoDB Howto

    +
    + +

    MongoDB Support in Postfix

    + +

    Postfix can use MongoDB as a source for any of its lookups: +aliases(5), virtual(5), canonical(5), etc. This allows you to keep +information for your mail service in a replicated noSQL database +with fine-grained access controls. By not storing it locally on the +mail server, the administrators can maintain it from anywhere, and +the users can control whatever bits of it you think appropriate. +You can have multiple mail servers using the same information, +without the hassle and delay of having to copy it to each.

    + +

    Topics covered in this document:

    + +
    + +

    Building Postfix with MongoDB support

    + +

    These instructions assume that you build Postfix from source +code as described in the INSTALL document. Some modification may +be required if you build Postfix from a vendor-specific source +package.

    + +

    The Postfix MongoDB client requires the mongo-c-driver library. +This can be built from source code from the +mongod-c project, or as a binary package from your OS distribution, +typically named mongo-c-driver-devel or libmongoc-dev. +Installing the mongo-c-driver library may also install libbson +as a dependency.

    + +

    To build Postfix with mongodb map support, add to the CCARGS +environment variable the options -DHAS_MONGODB and -I for the +directory containing the mongodb headers, and specify the AUXLIBS_MONGODB +with the libmongoc and libbson libraries, for example:

    + +
    +
    +% make tidy
    +% make -f Makefile.init makefiles \
    +    CCARGS="$CCARGS -DHAS_MONGODB -I/usr/include/libmongoc-1.0 \
    +    -I/usr/include/libbson-1.0" \
    +    AUXLIBS_MONGODB="-lmongoc-1.0 -lbson-1.0"
    +
    +
    + +

    The 'make tidy' command is needed only if you have previously +built Postfix without MongoDB support.

    + +

    If your MongoDB shared library is in a directory that the RUN-TIME +linker does not know about, add a "-Wl,-R,/path/to/directory" option +after "-lbson-1.0". Then, just run 'make'.

    + +

    Configuring MongoDB lookups

    + +

    In order to use MongoDB lookups, define a MongoDB source as a +table lookup in main.cf, for example:

    + +
    +
    +alias_maps = hash:/etc/aliases, proxy:mongodb:/etc/postfix/mongo-aliases.cf
    +
    +
    + +

    The file /etc/postfix/mongo-aliases.cf can specify a number of +parameters. For a complete description, see the mongodb_table(5) +manual page.

    + +

    Example: virtual(5) alias maps

    + +

    Here's a basic example for using MongoDB to look up virtual(5) +aliases. Assume that in main.cf, you have:

    + +
    +
    +virtual_alias_maps = hash:/etc/postfix/virtual_aliases, 
    +    proxy:mongodb:/etc/postfix/mongo-virtual-aliases.cf
    +
    +
    + +

    and in mongodb:/etc/postfix/mongo-virtual-aliases.cf you have:

    + +
    +
    +uri = mongodb+srv://user_name:password@some_server
    +dbname = mail
    +collection = mailbox
    +query_filter = {"$or": [{"username":"%s"}, {"alias.address": "%s"}], "active": 1}
    +result_attribute = username
    +
    +
    + +

    This example assumes mailbox names are stored in a MongoDB backend, +in a format like:

    + +
    +
    +{ "username": "user@example.com",
    +  "alias": [
    +    {"address": "admin@example.com"},
    +    {"address": "abuse@example.com"}
    +  ],
    +  "active": 1
    +}
    +
    +
    + +

    Upon receiving mail for "admin@example.com" that isn't found in the +/etc/postfix/virtual_aliases database, Postfix will search the +MongoDB server/cluster listening at port 27017 on some_server. It +will connect using the provided credentials, and search for any +entries whose username is, or alias field has "admin@example.com". +It will return the username attribute of those found, and build a +list of their email addresses.

    + +

    Example: Mailing lists

    + +

    When it comes to mailing lists, one way of implementing one would +be as below:

    + +
    +
    +{ "name": "dev@example.com", "active": 1, "address": 
    +  [ "hamid@example.com", "wietse@example.com", "viktor@example.com" ] }
    +
    +
    + +

    using the filter below, will result in a comma separated string +with all email addresses in this list.

    + +
    +
    +query_filter = {"name": "%s", "active": 1}
    +result_attribute = address
    +
    +
    + +

    Notes:

    + + + +

    Example: advanced projections

    + +

    This module also supports the use of more complex MongoDB +projections. There may be some use cases where operations such as +concatenation are necessary to be performed on the data retrieved +from the database. Although it is encouraged to keep the database +design simple enough so this is not necessary, postfix supports the +use of MongoDB projections to achieve the goal.

    + +

    Consider the example below:

    + +
    +
    +{ "username": "user@example.com",
    +  "local_part": "user",
    +  "domain": "example.com",
    +  "alias": [
    +    {"address": "admin@example.com"},
    +    {"address": "abuse@example.com"}
    +  ],
    +  "active": 1
    +}
    +
    +
    + +

    virtual_mailbox_maps can be created using below parameters in a +mongodb:/etc/postfix/mongo-virtual-mailboxes.cf file:

    + +
    +
    +uri = mongodb+srv://user_name:password@some_server
    +dbname = mail
    +collection = mailbox
    +query_filter = {"$or": [{"username":"%s"}, {"alias.address": "%s"}], "active": 1}
    +projection = { "mail_path": {"$concat": ["$domain", "/", "$local_part"]} }
    +
    +
    + +

    This will return 'example.com/user' path built from the database fields.

    + +

    A couple of considerations when using projections:

    + + +

    Feedback

    + +

    If you have questions, send them to postfix-users@postfix.org. +Please include relevant information about your Postfix setup: +MongoDB-related output from postconf, which libraries you built +with, and such. If your question involves your database contents, +please include the applicable bits of some database entries.

    + + + + diff --git a/postfix/proto/Makefile.in b/postfix/proto/Makefile.in index 511bd4448..ad7f73e50 100644 --- a/postfix/proto/Makefile.in +++ b/postfix/proto/Makefile.in @@ -30,6 +30,7 @@ HTML = ../html/ADDRESS_CLASS_README.html \ ../html/LMDB_README.html \ ../html/MEMCACHE_README.html \ ../html/MILTER_README.html \ + ../html/MONGODB_README.html \ ../html/MULTI_INSTANCE_README.html \ ../html/MYSQL_README.html ../html/NFS_README.html \ ../html/OVERVIEW.html \ @@ -78,6 +79,7 @@ README = ../README_FILES/ADDRESS_CLASS_README \ ../README_FILES/LMDB_README \ ../README_FILES/MEMCACHE_README \ ../README_FILES/MILTER_README \ + ../README_FILES/MONGODB_README \ ../README_FILES/MULTI_INSTANCE_README \ ../README_FILES/MYSQL_README ../README_FILES/NFS_README \ ../README_FILES/OVERVIEW \ @@ -240,6 +242,9 @@ clobber: ../html/MILTER_README.html: MILTER_README.html $(DETAB) $? | $(POSTLINK) >$@ +../html/MONGODB_README.html: MONGODB_README.html + $(DETAB) $? | $(POSTLINK) >$@ + ../html/MULTI_INSTANCE_README.html: MULTI_INSTANCE_README.html $(DETAB) $? | $(POSTLINK) >$@ @@ -420,6 +425,9 @@ clobber: ../README_FILES/MILTER_README: MILTER_README.html $(DETAB) $? | $(HT2READ) >$@ +../README_FILES/MONGODB_README: MONGODB_README.html + $(DETAB) $? | $(HT2READ) >$@ + ../README_FILES/MULTI_INSTANCE_README: MULTI_INSTANCE_README.html $(DETAB) $? | $(HT2READ) >$@ diff --git a/postfix/proto/mongodb_table b/postfix/proto/mongodb_table new file mode 100644 index 000000000..81dfc8e4d --- /dev/null +++ b/postfix/proto/mongodb_table @@ -0,0 +1,240 @@ +#++ +# NAME +# mongodb_table 5 +# SUMMARY +# Postfix MongoDB client configuration +# SYNOPSIS +# \fBpostmap -q "\fIstring\fB" mongodb:/etc/postfix/\fIfilename\fR +# +# \fBpostmap -q - mongodb:/etc/postfix/\fIfilename\fB <\fIinputfile\fR +# DESCRIPTION +# The Postfix mail system uses optional tables for address +# rewriting or mail routing. These tables are usually in +# \fBdbm\fR or \fBdb\fR format. +# +# Alternatively, lookup tables can be specified as MongoDB +# databases. In order to use MongoDB lookups, define a MongoDB +# source as a lookup table in main.cf, for example: +# .nf +# alias_maps = mongodb:/etc/postfix/mongodb-aliases.cf +# .fi +# +# In this example, the file /etc/postfix/mongodb-aliases.cf +# has the same format as the Postfix main.cf file, and can +# specify the parameters described below. It is also possible +# to have the configuration in main.cf; see "OBSOLETE MAIN.CF +# PARAMETERS" below. +# +# It is strongly recommended to use proxy:mongodb, in order +# to reduce the number of database connections. For example: +# .nf +# alias_maps = proxy:mongodb:/etc/postfix/mongodb-aliases.cf +# .fi +# +# Note: when using proxy:mongodb:/\fIfile\fR, the file must +# be readable by the unprivileged postfix user (specified +# with the Postfix mail_owner configuration parameter). +# MONGODB PARAMETERS +# .ad +# .fi +# .IP "\fBuri\fR" +# The URI of mongo server/cluster that Postfix will try to +# connect to and query from. Please see +# .nf +# https://www.mongodb.com/docs/manual/reference/connection-string/ +# .fi +# +# Example: +# .nf +# uri = mongodb+srv://user:pass@loclhost:27017/mail +# .fi +# .IP "\fBdbname\fR" +# Name of the database to read the information from. +# Example: +# .nf +# dbname = mail +# .fi +# .IP "\fBcollection\fR" +# Name of the collection (table) to read the information from. +# Example: +# .nf +# collection = mailbox +# .fi +# .IP "\fBquery_filter\fR" +# The MongoDB query template used to search the database, +# where \fB%s\fR is a substitute for the email address that +# Postfix is trying to resolve. Please see: +# .nf +# https://www.mongodb.com/docs/manual/tutorial/query-documents/ +# .fi +# +# Example: +# .nf +# query_filter = {"$or": [{"username": "%s"}, {"alias.address": "%s"}], "active": 1} +# .fi +# +# This parameter supports the following '%' expansions: +# .RS +# .IP "\fB%%\fR" +# This is replaced by a literal '%' character. +# .IP "\fB%s\fR" +# This is replaced by the input key. The %s must appear in +# quotes, because all Postfix queries are strings containing +# (parts from) a domain or email address. Postfix makes no +# numerical queries. +# .IP "\fB%u\fR" +# When the input key is an address of the form user@domain, +# \fB%u\fR is replaced by the local part of the address. +# Otherwise, \fB%u\fR is replaced by the entire search string. +# .IP "\fB%d\fR" +# When the input key is an address of the form user@domain, +# \fB%d\fR is replaced by the domain part of the address. +# .IP "\fB%[1-9]\fR" +# The patterns %1, %2, ... %9 are replaced by the corresponding +# most significant component of the input key's domain. If +# the input key is \fIuser@mail.example.com\fR, then %1 is +# \fBcom\fR, %2 is \fBexample\fR and %3 is \fBmail\fR. +# .RE +# .IP +# In the above substitutions, characters will be quoted as +# required by RFC 4627. For example, each double quote or +# backslash character will be escaped with a backslash +# characacter. +# .IP "\fBprojection\fR" +# Advanced MongoDB query projections. Please see: +# .nf +# https://www.mongodb.com/docs/manual/tutorial/project-fields-from-query-results/ +# .fi +# +# .RS +# .IP \(bu +# If \fBprojection\fR is non-empty, then \fBresult_attribute\fR +# must be empty. +# .IP \(bu +# This implementation can extract information only from result +# fields that have type \fBstring\fR (UTF8), \fBinteger\fR +# (int32, int64) and \fBarray\fR. Other result fields will +# be ignored with a warning. Please see: +# .nf +# https://mongoc.org/libbson/current/bson_type_t.html +# .fi +# .IP \(bu +# As with \fBresult_attribute\fR, the top-level _id field +# (type OID) is automatically removed from projection results. +# .RE +# .IP "\fBresult_attribute\fR" +# Comma or whitespace separated list with the names of fields +# to be returned in a lookup result. +# +# .RS +# .IP \(bu +# If \fBresult_attribute\fR is non-empty, then \fBprojection\fR +# must be empty. +# .IP \(bu +# As with \fBprojection\fR, the top-level _id field (type +# OID) is automatically removed from lookup results. +# .RE +# .IP "\fBresult_format (default: \fB%s\fR)\fR" +# Format template applied to the result from \fBprojection\fR +# or \fBresult_attribute\fR. Most commonly used to append (or +# prepend) text to the result. This parameter supports the +# following '%' expansions: +# .RS +# .IP "\fB%%\fR" +# This is replaced by a literal '%' character. +# .IP "\fB%s\fR" +# This is replaced by the value of the result attribute. When +# result is empty it is skipped. +# .IP "\fB%u\fR +# When the result attribute value is an address of the form +# user@domain, \fB%u\fR is replaced by the local part of the +# address. When the result has an empty localpart it is +# skipped. +# .IP "\fB%d\fR" +# When a result attribute value is an address of the form +# user@domain, \fB%d\fR is replaced by the domain part of the +# attribute value. When the result is unqualified it is +# skipped. +# .IP "\fB%[SUD1-9]\fR" +# The upper-case and decimal digit expansions interpolate the +# parts of the input key rather than the result. Their behavior +# is identical to that described with \fBquery_filter\fR, and +# in fact because the input key is known in advance, lookups +# whose key does not contain all the information specified +# in the result template are suppressed and return no results. +# .RE +# .IP +# For example, using "result_format = smtp:[%s]" allows one +# to use a mailHost attribute as the basis of a transport(5) +# table. After applying the result format, multiple values +# are concatenated as comma separated strings. The expansion_limit +# parameter explained below allows one to restrict the number +# of values in the result, which is especially useful for +# maps that should return a single value. +# +# The default value \fB%s\fR specifies that each +# attribute value should be used as is. +# +# NOTE: DO NOT put quotes around the result format! The result +# is not a JSON string. +# .IP "\fBdomain (default: no domain list)\fR" +# This is a list of domain names, paths to files, or "type:table" +# databases. When specified, only fully qualified search keys +# with a *non-empty* localpart and a matching domain are +# eligible for lookup: 'user' lookups, bare domain lookups +# and "@domain" lookups are not performed. This can significantly +# reduce the query load on the backend database. Example: +# .nf +# domain = postfix.org, hash:/etc/postfix/searchdomains +# .fi +# .IP "\fBexpansion_limit (default: 0)\fR" +# A limit on the total number of result elements returned (as +# a comma separated list) by a lookup against the map. A +# setting of zero disables the limit. Lookups fail with a +# temporary error if the limit is exceeded. Setting the limit +# to 1 ensures that lookups do not return multiple values. +# OBSOLETE MAIN.CF PARAMETERS +# .ad +# .fi +# MongoDB parameters can also be defined in main.cf. Specify +# as MongoDB source a name that doesn't begin with a slash +# or a dot. The MongoDB parameters will then be accessible +# as the name you've given the source in its definition, an +# underscore, and the name of the parameter. For example, if +# a map is specified as "mongodb:\fImongodb_source\fR", the +# "uri" parameter would be defined in main.cf as +# "\fImongodb_source\fR_uri". +# +# Note: with this form, passwords are written in main.cf, +# which is normally world-readable, and '$' in a mongodb +# parameter setting needs to be written as '$$'. +# SEE ALSO +# postmap(1), Postfix lookup table maintenance +# postconf(5), configuration parameters +# README FILES +# .ad +# .fi +# Use "\fBpostconf readme_directory\fR" or "\fBpostconf +# html_directory\fR" to locate this information. +# .na +# .nf +# DATABASE_README, Postfix lookup table overview +# MONGODB_README, Postfix MONGODB client guide +# LICENSE +# .ad +# .fi +# The Secure Mailer license must be distributed with this software. +# HISTORY +# MongoDB support was introduced with Postfix version 3.9. +# AUTHOR(S) +# Hamid Maadani (hamid@dexo.tech) +# Dextrous Technologies, LLC +# +# Edited by: +# Wietse Venema +# porcupine.org +# +# Based on prior work by: +# Stephan Ferraro +# Aionda GmbH +#-- diff --git a/postfix/proto/stop b/postfix/proto/stop index c9b260241..37436858b 100644 --- a/postfix/proto/stop +++ b/postfix/proto/stop @@ -1595,3 +1595,4 @@ EOD chunking allowlists FWS +mongodb diff --git a/postfix/proto/stop.double-proto-html b/postfix/proto/stop.double-proto-html index e1df531f7..d79c684ee 100644 --- a/postfix/proto/stop.double-proto-html +++ b/postfix/proto/stop.double-proto-html @@ -355,3 +355,5 @@ RFC 2045 Sections 2 7 and 2 8 br br Such clients can be to become a list of comma separated names br br This feature the form of a domain name hostname hostname service hostname service expected to become a list of comma separated names br br This +Postfix Postfix can use MongoDB as a source for any of its lookups aliases 5 virtual 5 canonical 5 etc This allows you to keep information for your mail service in a replicated noSQL database with fine grained access controls By not storing it + CCARGS CCARGS DHAS_MONGODB I usr include libmongoc 1 0 diff --git a/postfix/proto/stop.spell-cc b/postfix/proto/stop.spell-cc index 9aa64c6e9..41d79beed 100644 --- a/postfix/proto/stop.spell-cc +++ b/postfix/proto/stop.spell-cc @@ -1814,3 +1814,23 @@ inlined stringz Sarvepalli uXXXX +Aionda +Ferraro +GmbH +Hamid +LLC +Maadani +MongoDB +PRId +bson +dexo +hamid +itoa +libmongoc +mongdb +mongo +mongodb +mongodbconf +Dextrous +Mongo +SUD diff --git a/postfix/proto/stop.spell-history b/postfix/proto/stop.spell-history index 16d0e729f..d5e53d3a8 100644 --- a/postfix/proto/stop.spell-history +++ b/postfix/proto/stop.spell-history @@ -70,3 +70,6 @@ dehtml NONPROD LC Philosof +MONGODB +Refactored +Vijay diff --git a/postfix/proto/stop.spell-proto-html b/postfix/proto/stop.spell-proto-html index 6baa193e5..79c1ce533 100644 --- a/postfix/proto/stop.spell-proto-html +++ b/postfix/proto/stop.spell-proto-html @@ -359,3 +359,18 @@ wraptls api MinProtocol spammy +concat +hamid +ina +lbson +libbson +libmobgo +libmongoc +lmongoc +mongo +mongod +noSQL +srv +viktor +MONGODB +MongoDB diff --git a/postfix/src/global/Makefile.in b/postfix/src/global/Makefile.in index 562290232..c7a1d36af 100644 --- a/postfix/src/global/Makefile.in +++ b/postfix/src/global/Makefile.in @@ -3,7 +3,7 @@ SRCS = abounce.c anvil_clnt.c been_here.c bounce.c bounce_log.c \ canon_addr.c cfg_parser.c cleanup_strerror.c cleanup_strflags.c \ clnt_stream.c conv_time.c db_common.c debug_peer.c debug_process.c \ defer.c deliver_completed.c deliver_flock.c deliver_pass.c \ - deliver_request.c dict_ldap.c dict_mysql.c dict_pgsql.c \ + deliver_request.c dict_ldap.c dict_mongodb.c dict_mysql.c dict_pgsql.c \ dict_proxy.c dict_sqlite.c domain_list.c dot_lockfile.c dot_lockfile_as.c \ dsb_scan.c dsn.c dsn_buf.c dsn_mask.c dsn_print.c dsn_util.c \ ehlo_mask.c ext_prop.c file_id.c flush_clnt.c header_opts.c \ @@ -80,13 +80,13 @@ OBJS = abounce.o anvil_clnt.o been_here.o bounce.o bounce_log.o \ # MAP_OBJ is for maps that may be dynamically loaded with dynamicmaps.cf. # When hard-linking these maps, makedefs sets NON_PLUGIN_MAP_OBJ=$(MAP_OBJ), # otherwise it sets the PLUGIN_* macros. -MAP_OBJ = dict_ldap.o dict_mysql.o dict_pgsql.o dict_sqlite.o +MAP_OBJ = dict_ldap.o dict_mysql.o dict_pgsql.o dict_sqlite.o dict_mongodb.o HDRS = abounce.h anvil_clnt.h been_here.h bounce.h bounce_log.h \ canon_addr.h cfg_parser.h cleanup_user.h clnt_stream.h config.h \ conv_time.h db_common.h debug_peer.h debug_process.h defer.h \ deliver_completed.h deliver_flock.h deliver_pass.h deliver_request.h \ - dict_ldap.h dict_mysql.h dict_pgsql.h dict_proxy.h dict_sqlite.h domain_list.h \ + dict_ldap.h dict_mysql.h dict_pgsql.h dict_mongodb.h dict_proxy.h dict_sqlite.h domain_list.h \ dot_lockfile.h dot_lockfile_as.h dsb_scan.h dsn.h dsn_buf.h \ dsn_mask.h dsn_print.h dsn_util.h ehlo_mask.h ext_prop.h \ file_id.h flush_clnt.h header_opts.h header_token.h input_transp.h \ @@ -136,7 +136,7 @@ LIBS = ../../lib/lib$(LIB_PREFIX)util$(LIB_SUFFIX) LIB_DIR = ../../lib INC_DIR = ../../include PLUGIN_MAP_SO = $(LIB_PREFIX)ldap$(LIB_SUFFIX) $(LIB_PREFIX)mysql$(LIB_SUFFIX) \ - $(LIB_PREFIX)pgsql$(LIB_SUFFIX) $(LIB_PREFIX)sqlite$(LIB_SUFFIX) + $(LIB_PREFIX)pgsql$(LIB_SUFFIX) $(LIB_PREFIX)sqlite$(LIB_SUFFIX) $(LIB_PREFIX)mongodb$(LIB_SUFFIX) MAKES = .c.o:; $(CC) $(SHLIB_CFLAGS) $(CFLAGS) -c $*.c @@ -173,6 +173,9 @@ $(LIB_PREFIX)pgsql$(LIB_SUFFIX): dict_pgsql.o $(LIB_PREFIX)sqlite$(LIB_SUFFIX): dict_sqlite.o $(PLUGIN_LD) $(SHLIB_RPATH) -o $@ dict_sqlite.o $(AUXLIBS_SQLITE) +$(LIB_PREFIX)mongodb$(LIB_SUFFIX): dict_mongodb.o + $(PLUGIN_LD) $(SHLIB_RPATH) -o $@ dict_mongodb.o $(AUXLIBS_MONGODB) + update: $(LIB_DIR)/$(LIB) $(HDRS) $(PLUGIN_MAP_SO_UPDATE) -for i in $(HDRS); \ do \ @@ -1210,6 +1213,24 @@ dict_memcache.o: dict_memcache.c dict_memcache.o: dict_memcache.h dict_memcache.o: memcache_proto.h dict_memcache.o: string_list.h +dict_mongodb.o: ../../include/argv.h +dict_mongodb.o: ../../include/auto_clnt.h +dict_mongodb.o: ../../include/check_arg.h +dict_mongodb.o: ../../include/dict.h +dict_mongodb.o: ../../include/match_list.h +dict_mongodb.o: ../../include/msg.h +dict_mongodb.o: ../../include/myflock.h +dict_mongodb.o: ../../include/mymalloc.h +dict_mongodb.o: ../../include/stringops.h +dict_mongodb.o: ../../include/sys_defs.h +dict_mongodb.o: ../../include/vbuf.h +dict_mongodb.o: ../../include/vstream.h +dict_mongodb.o: ../../include/vstring.h +dict_mongodb.o: cfg_parser.h +dict_mongodb.o: db_common.h +dict_mongodb.o: dict_mongodb.c +dict_mongodb.o: dict_mongodb.h +dict_mongodb.o: string_list.h dict_mysql.o: ../../include/argv.h dict_mysql.o: ../../include/check_arg.h dict_mysql.o: ../../include/dict.h @@ -1861,6 +1882,7 @@ mail_dict.o: ../../include/vstream.h mail_dict.o: ../../include/vstring.h mail_dict.o: dict_ldap.h mail_dict.o: dict_memcache.h +mail_dict.o: dict_mongodb.h mail_dict.o: dict_mysql.h mail_dict.o: dict_pgsql.h mail_dict.o: dict_proxy.h diff --git a/postfix/src/global/dict_mongodb.c b/postfix/src/global/dict_mongodb.c new file mode 100644 index 000000000..1aa36b3f2 --- /dev/null +++ b/postfix/src/global/dict_mongodb.c @@ -0,0 +1,569 @@ +/*++ +/* NAME +/* dict_mongodb 3 +/* SUMMARY +/* dictionary interface to mongodb, compatible with libmongoc-1.0 +/* SYNOPSIS +/* #include +/* +/* DICT *dict_mongodb_open(name, open_flags, dict_flags) +/* const char *name; +/* int open_flags; +/* int dict_flags; +/* DESCRIPTION +/* dict_mongodb_open() opens a MongoDB database, providing a +/* dictionary interface for Postfix mappings. The result is a +/* pointer to the installed dictionary. +/* +/* Configuration parameters are described in mongodb_table(5). +/* +/* Arguments: +/* .IP name +/* Either the path to the MongoDB configuration file (if it +/* starts with '/' or '.'), or the prefix which will be used +/* to obtain main.cf configuration parameters for this search. +/* +/* In the first case, configuration parameters are specified +/* in the file as \fIname\fR=\fIvalue\fR pairs. +/* +/* In the second case, the configuration parameters are prefixed +/* with the value of \fIname\fR and an underscore, and they +/* are specified in main.cf. For example, if this value is +/* \fImongodbconf\fR, the parameters would look like +/* \fImongodbconf_uri\fR, \fImongodbconf_collection\fR, and +/* so on. +/* .IP open_flags +/* Must be O_RDONLY +/* .IP dict_flags +/* See dict_open(3). +/* SEE ALSO +/* dict(3) generic dictionary manager +/* HISTORY +/* .ad +/* .fi +/* MongoDB support was added in Postfix 3.9. +/* AUTHOR(S) +/* Hamid Maadani (hamid@dexo.tech) +/* Dextrous Technologies, LLC +/* +/* Edited by: +/* Wietse Venema +/* porcupine.org +/* +/* Based on prior work by: +/* Stephan Ferraro +/* Aionda GmbH +/*--*/ + + /* + * System library. + */ +#include +#ifdef HAS_MONGODB +#include +#include +#include +#include +#include +#include /* C99 PRId64 */ + +#include +#include + + /* + * Utility library. + */ +#include +#include +#include +#include +#include +#include +#include + + /* + * Global library. + */ +#include +#include + + /* + * Application-specific. + */ +#include + + /* + * Initial size for dynamically-allocated buffers. + */ +#ifndef BUFFER_SIZE +#define BUFFER_SIZE 1024 +#endif + +#define INIT_VSTR(buf, len) do { \ + if (buf == 0) \ + buf = vstring_alloc(len); \ + VSTRING_RESET(buf); \ + VSTRING_TERMINATE(buf); \ + } while (0) + +/* Structure of one mongodb dictionary handle. */ +typedef struct { + /* Initialized by dict_mongodb_open(). */ + DICT dict; /* Parent class */ + CFG_PARSER *parser; /* Configuration file parser */ + mongoc_client_t *client; /* Mongo C client handle */ + /* Initialized by mongodb_parse_config(). */ + char *uri; /* mongodb+srv:/*localhost:27017 */ + char *dbname; /* Database name */ + char *collection; /* Collection name */ + char *query_filter; /* db_common_expand() query template */ + char *projection; /* Advanced MongoDB projection */ + char *result_attribute; /* The key(s) to return the data for */ + char *result_format; /* db_common_expand() result_template */ + int expansion_limit; /* Result expansion limit */ + void *ctx; /* db_common handle */ +} DICT_MONGODB; + +/* Per-process initialization. */ +static bool init_done = false; + +/* itoa - int64_t to string */ + +static char *itoa(int64_t val) +{ + static char buf[21] = {0}; + int ret; + + /* + * XXX(Wietse) replaced custom code with standard library calls that + * handle zero, and negative values. + */ +#define PRId64_FORMAT "%" PRId64 + + ret = snprintf(buf, sizeof(buf), PRId64_FORMAT, val); + if (ret < 0) + msg_panic("itoa: output error for '%s'", PRId64_FORMAT); + if (ret >= sizeof(buf)) + msg_panic("itoa: output for '%s' exceeds space %ld", + PRId64_FORMAT, sizeof(buf)); + return (buf); +} + +/* mongodb_parse_config - parse mongodb configuration file */ + +static void mongodb_parse_config(DICT_MONGODB *dict_mongodb, + const char *mongodbcf) +{ + CFG_PARSER *p = dict_mongodb->parser; + + /* + * Parse the configuration file. + */ + dict_mongodb->uri = cfg_get_str(p, "uri", NULL, 1, 0); + dict_mongodb->dbname = cfg_get_str(p, "dbname", NULL, 1, 0); + dict_mongodb->collection = cfg_get_str(p, "collection", NULL, 1, 0); + dict_mongodb->query_filter = cfg_get_str(p, "query_filter", NULL, 1, 0); + + /* + * One of projection and result_attribute must be specified. That is + * enforced in the caller. + */ + dict_mongodb->projection = cfg_get_str(p, "projection", NULL, 0, 0); + dict_mongodb->result_attribute + = cfg_get_str(p, "result_attribute", NULL, 0, 0); + dict_mongodb->result_format + = cfg_get_str(dict_mongodb->parser, "result_format", "%s", 1, 0); + dict_mongodb->expansion_limit + = cfg_get_int(dict_mongodb->parser, "expansion_limit", 10, 0, 100); + + /* + * db_common query parsing and domain pattern lookup. + */ + dict_mongodb->ctx = 0; + (void) db_common_parse(&dict_mongodb->dict, &dict_mongodb->ctx, + dict_mongodb->query_filter, 1); + db_common_parse_domain(dict_mongodb->parser, dict_mongodb->ctx); +} + +/* expand_value - expand lookup result value */ + +static bool expand_value(DICT_MONGODB *dict_mongodb, const char *p, + const char *lookup_name, + VSTRING *resultString, + int *expansion, const char *key) +{ + + /* + * If a lookup result cannot be processed due to an expansion limit + * error, return a DICT_ERR_RETRY error code and a 'false' result value. + * As documented for many dict_xxx() implementations, and expansion limit + * error is considered a temporary error. + */ + if (dict_mongodb->expansion_limit > 0 + && ++(*expansion) > dict_mongodb->expansion_limit) { + msg_warn("%s:%s: expansion limit exceeded for key: '%s'", + dict_mongodb->dict.type, dict_mongodb->dict.name, key); + dict_mongodb->dict.error = DICT_ERR_RETRY; + return (false); + } + + /* + * XXX(Wietse) Added the dict_mongodb_lookup() lookup_name argument, + * because it selects code paths inside db_common_expand() that are + * specifically for lookup results instead of lookup keys, including + * %[SUD] substitution. + */ + db_common_expand(dict_mongodb->ctx, dict_mongodb->result_format, p, + lookup_name, resultString, 0); + return (true); +} + +/* get_result_string - convert lookup result to string, or set dict.error */ + +static char *get_result_string(DICT_MONGODB *dict_mongodb, + VSTRING *resultString, + bson_iter_t *iter, + const char *lookup_name, + int *expansion, + const char *key) +{ + char *p = NULL; + bool got_one_result = false; + + /* + * If a lookup result cannot be processed due to an error, return a + * non-zero error code and a NULL result value. + */ + INIT_VSTR(resultString, BUFFER_SIZE); + while (dict_mongodb->dict.error == DICT_ERR_NONE && bson_iter_next(iter)) { + switch (bson_iter_type(iter)) { + case BSON_TYPE_UTF8: + p = (char *) bson_iter_utf8(iter, NULL); + if (!bson_utf8_validate(p, strlen(p), true)) { + msg_warn("%s:%s: invalid UTF-8 in lookup result '%s'", + dict_mongodb->dict.type, dict_mongodb->dict.name, p); + dict_mongodb->dict.error = DICT_ERR_RETRY; + break; + } + got_one_result |= expand_value(dict_mongodb, p, lookup_name, + resultString, expansion, key); + break; + case BSON_TYPE_INT64: + case BSON_TYPE_INT32: + p = itoa(bson_iter_as_int64(iter)); + got_one_result |= expand_value(dict_mongodb, p, lookup_name, + resultString, expansion, key); + break; + case BSON_TYPE_ARRAY: + const uint8_t *dataBuffer = NULL; + unsigned int len = 0; + bson_iter_t dataIter; + bson_t *data = NULL; + + /* + * XXX(Wietse) are there any non-error cases, such as a valid but + * empty array, where bson_new_from_data() or bson_iter_init() + * would return null or false? If there are no such cases then we + * must handle null/false as an error. + */ + bson_iter_array(iter, &len, &dataBuffer); + if ((data = bson_new_from_data(dataBuffer, len)) != 0 + && bson_iter_init(&dataIter, data)) { + VSTRING *iterResult = vstring_alloc(BUFFER_SIZE); + + if ((p = get_result_string(dict_mongodb, iterResult, &dataIter, + lookup_name, expansion, key)) != 0) { + vstring_sprintf_append(resultString, (got_one_result) ? + ",%s" : "%s", p); + got_one_result |= true; + } + vstring_free(iterResult); + } + bson_destroy(data); + break; + default: + /* Unexpected field type. As documented, warn and ignore. */ + msg_warn("%s:%s: failed to retrieve value of '%s', " + "Unknown result type %d.", dict_mongodb->dict.type, + dict_mongodb->dict.name, bson_iter_key(iter), + bson_iter_type(iter)); + break; + } + } + if (dict_mongodb->dict.error != DICT_ERR_NONE || !got_one_result) + return (0); + return (vstring_str(resultString)); +} + +/* dict_mongdb_quote - quote json string */ + +static void dict_mongdb_quote(DICT *dict, const char *name, VSTRING *result) +{ + /* quote_for_json_append() will resize the result buffer as needed. */ + (void) quote_for_json_append(result, name, -1); +} + +/* dict_mongdb_append_result_attributes - projection builder */ + +static int dict_mongdb_append_result_attribute(bson_t * projection, + const char *result_attribute) +{ + char *ra = mystrdup(result_attribute); + char *pp = ra; + char *cp; + int ok = 1; + + while (ok && (cp = mystrtok(&pp, CHARS_COMMA_SP)) != 0) + ok = BSON_APPEND_INT32(projection, cp, 1); + myfree(ra); + return (ok); +} + +/* dict_mongodb_lookup - find database entry using mongo query language */ + +static const char *dict_mongodb_lookup(DICT *dict, const char *name) +{ + DICT_MONGODB *dict_mongodb = (DICT_MONGODB *) dict; + mongoc_collection_t *coll = NULL; + mongoc_cursor_t *cursor = NULL; + bson_iter_t iter; + const bson_t *doc = NULL; + bson_t *query = NULL; + bson_t *options = NULL; + bson_t *projection = NULL; + bson_error_t error; + char *result = NULL; + static VSTRING *queryString = NULL; + static VSTRING *resultString = NULL; + int domain_rc; + int expansion = 0; + + dict_mongodb->dict.error = DICT_ERR_NONE; + + /* + * If they specified a domain list for this map, then only search for + * addresses in domains on the list. This can significantly reduce the + * load on the database. + */ + if ((domain_rc = db_common_check_domain(dict_mongodb->ctx, name)) == 0) { + if (msg_verbose) + msg_info("%s:%s: skipping lookup of '%s': domain mismatch", + dict_mongodb->dict.type, dict_mongodb->dict.name, name); + return (0); + } else if (domain_rc < 0) { + DICT_ERR_VAL_RETURN(dict, domain_rc, (char *) 0); + } + + /* + * Ugly macros to make error and non-error handling code more readable. + * If code size is a concern, them an optimizing compiler can eliminate + * dead code or duplicated code. + */ + + /* Set an error code, and return null. */ +#define DICT_MONGODB_LOOKUP_ERR_RETURN(err) do { \ + dict_mongodb->dict.error = (err); \ + DICT_MONGODB_LOOKUP_RETURN((char *) 0); \ +} while (0); + + /* Pass through any error, and return the specified value. */ +#define DICT_MONGODB_LOOKUP_RETURN(val) do { \ + if (coll) mongoc_collection_destroy(coll); \ + if (cursor) mongoc_cursor_destroy(cursor); \ + if (query) bson_destroy(query); \ + if (options) bson_destroy(options); \ + if (projection) bson_destroy(projection); \ + return (val); \ + } while (0) + + coll = mongoc_client_get_collection(dict_mongodb->client, + dict_mongodb->dbname, + dict_mongodb->collection); + if (!coll) { + msg_warn("%s:%s: failed to get collection [%s] from [%s]", + dict_mongodb->dict.type, dict_mongodb->dict.name, + dict_mongodb->collection, dict_mongodb->dbname); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + + /* + * Use the specified result projection, or craft one from the + * result_attribute. Exclude the _id field from the result. + */ + options = bson_new(); + if (dict_mongodb->projection) { + projection = bson_new_from_json((uint8_t *) dict_mongodb->projection, + -1, &error); + if (!projection) { + msg_warn("%s:%s: failed to create a projection from '%s': %s", + dict_mongodb->dict.type, dict_mongodb->dict.name, + dict_mongodb->projection, error.message); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + if (!BSON_APPEND_INT32(projection, "_id", 0) + || !BSON_APPEND_DOCUMENT(options, "projection", projection)) { + msg_warn("%s:%s: failed to append a projection from '%s'", + dict_mongodb->dict.type, dict_mongodb->dict.name, + dict_mongodb->projection); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + } else if (dict_mongodb->result_attribute) { + bson_t res_attr; + + if (!BSON_APPEND_DOCUMENT_BEGIN(options, "projection", &res_attr) + || !BSON_APPEND_INT32(&res_attr, "_id", 0) + || !dict_mongdb_append_result_attribute(&res_attr, + dict_mongodb->result_attribute) + || !bson_append_document_end(options, &res_attr)) { + msg_warn("%s:%s: failed to append a projection from '%s'", + dict_mongodb->dict.type, dict_mongodb->dict.name, + dict_mongodb->result_attribute); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + } else { + /* Can't happen. The configuration parser should reject this. */ + msg_panic("%s:%s: empty 'projection' and 'result_attribute'", + dict_mongodb->dict.type, dict_mongodb->dict.name); + } + + /* + * Expand filter template. This uses a quoting function to prevent + * metacharacter injection with parts from a crafted email address. + */ + INIT_VSTR(queryString, BUFFER_SIZE); + if (!db_common_expand(dict_mongodb->ctx, dict_mongodb->query_filter, + name, 0, queryString, dict_mongdb_quote)) + /* Suppress the actual lookup if the expansion is empty. */ + DICT_MONGODB_LOOKUP_RETURN(0); + + /* Create the query from the expanded query template. */ + query = bson_new_from_json((uint8_t *) vstring_str(queryString), + -1, &error); + if (!query) { + msg_warn("%s:%s: failed to create a query from '%s': %s", + dict_mongodb->dict.type, dict_mongodb->dict.name, + vstring_str(queryString), error.message); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + /* Run the query. */ + cursor = mongoc_collection_find_with_opts(coll, query, options, NULL); + if (mongoc_cursor_error(cursor, &error)) { + msg_warn("%s:%s: cursor error for '%s': %s", + dict_mongodb->dict.type, dict_mongodb->dict.name, + vstring_str(queryString), error.message); + DICT_MONGODB_LOOKUP_ERR_RETURN(DICT_ERR_RETRY); + } + /* Convert the lookup result to C string. */ + INIT_VSTR(resultString, BUFFER_SIZE); + while (mongoc_cursor_next(cursor, &doc)) { + if (bson_iter_init(&iter, doc)) { + result = get_result_string(dict_mongodb, resultString, &iter, + name, &expansion, name); + } + } + DICT_MONGODB_LOOKUP_RETURN(result); +} + +/* dict_mongodb_close - close MongoDB database */ + +static void dict_mongodb_close(DICT *dict) +{ + DICT_MONGODB *dict_mongodb = (DICT_MONGODB *) dict; + + cfg_parser_free(dict_mongodb->parser); + if (dict_mongodb->ctx) { + db_common_free_ctx(dict_mongodb->ctx); + } + myfree(dict_mongodb->uri); + myfree(dict_mongodb->dbname); + myfree(dict_mongodb->collection); + myfree(dict_mongodb->query_filter); + + if (dict_mongodb->result_attribute) { + myfree(dict_mongodb->result_attribute); + } + if (dict_mongodb->result_format) { + myfree(dict_mongodb->result_format); + } + if (dict_mongodb->projection) { + myfree(dict_mongodb->projection); + } + if (dict_mongodb->client) { + mongoc_client_destroy(dict_mongodb->client); + } + dict_free(dict); +} + +/* dict_mongodb_open - open MongoDB database connection */ + +DICT *dict_mongodb_open(const char *name, int open_flags, int dict_flags) +{ + DICT_MONGODB *dict_mongodb; + CFG_PARSER *parser; + mongoc_uri_t *uri = 0; + bson_error_t error; + + /* Sanity checks. */ + if (open_flags != O_RDONLY) { + return (dict_surrogate(DICT_TYPE_MONGODB, name, open_flags, dict_flags, + "%s:%s: map requires O_RDONLY access mode", + DICT_TYPE_MONGODB, name)); + } + /* Open the configuration file. */ + if ((parser = cfg_parser_alloc(name)) == 0) { + return (dict_surrogate(DICT_TYPE_MONGODB, name, open_flags, dict_flags, + "open %s: %m", name)); + } + /* Create the dictionary object. */ + dict_mongodb = (DICT_MONGODB *) dict_alloc(DICT_TYPE_MONGODB, name, + sizeof(*dict_mongodb)); + dict_mongodb->dict.lookup = dict_mongodb_lookup; + dict_mongodb->dict.close = dict_mongodb_close; + dict_mongodb->dict.flags = dict_flags; + dict_mongodb->parser = parser; + dict_mongodb->dict.owner = cfg_get_owner(dict_mongodb->parser); + dict_mongodb->client = NULL; + + /* Parse config. */ + mongodb_parse_config(dict_mongodb, name); + if (!dict_mongodb->projection == !dict_mongodb->result_attribute) { + dict_mongodb_close(&dict_mongodb->dict); + return (dict_surrogate(DICT_TYPE_MONGODB, name, open_flags, dict_flags, + "%s:%s: specify exactly one of 'projection' or 'result_attribute'", + DICT_TYPE_MONGODB, name)); + } + /* One-time initialization of libmongoc 's internals. */ + if (!init_done) { + mongoc_init(); + init_done = true; + } +#define DICT_MONGODB_OPEN_ERR_RETURN(d) do { \ + DICT *_d = (d); \ + if (uri) mongoc_uri_destroy(uri); \ + dict_mongodb_close(&dict_mongodb->dict); \ + return (_d); \ + } while (0); + + uri = mongoc_uri_new_with_error(dict_mongodb->uri, &error); + if (!uri) + DICT_MONGODB_OPEN_ERR_RETURN(dict_surrogate(DICT_TYPE_MONGODB, name, + open_flags, dict_flags, + "%s:%s: failed to parse URI '%s': %s", + DICT_TYPE_MONGODB, name, + dict_mongodb->uri, error.message)); + + dict_mongodb->client = mongoc_client_new_from_uri_with_error(uri, &error); + if (!dict_mongodb->client) + DICT_MONGODB_OPEN_ERR_RETURN(dict_surrogate(DICT_TYPE_MONGODB, name, + open_flags, dict_flags, + "%s:%s: failed to create client for '%s': %s", + DICT_TYPE_MONGODB, name, + dict_mongodb->uri, + error.message)); + + mongoc_uri_destroy(uri); + mongoc_client_set_error_api(dict_mongodb->client, MONGOC_ERROR_API_VERSION_2); + return (DICT_DEBUG (&dict_mongodb->dict)); +} + +#endif diff --git a/postfix/src/global/dict_mongodb.h b/postfix/src/global/dict_mongodb.h new file mode 100755 index 000000000..d5120cb05 --- /dev/null +++ b/postfix/src/global/dict_mongodb.h @@ -0,0 +1,43 @@ +#ifndef _DICT_MONGODB_INCLUDED_ +#define _DICT_MONGODB_INCLUDED_ + +/*++ +/* NAME +/* dict_mongodb 3h +/* SUMMARY +/* dictionary interface to mongodb databases +/* SYNOPSIS +/* #include +/* DESCRIPTION +/* .nf + + /* + * Utility library. + */ +#include + + /* + * External interface. + */ +#define DICT_TYPE_MONGODB "mongodb" + +extern DICT *dict_mongodb_open(const char *, int, int); + +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Hamid Maadani (hamid@dexo.tech) +/* Dextrous Technologies, LLC +/* +/* Edited by: +/* Wietse Venema +/* porcupine.org +/* +/* Based on prior work by: +/* Stephan Ferraro +/* Aionda GmbH +/*--*/ + +#endif diff --git a/postfix/src/global/mail_dict.c b/postfix/src/global/mail_dict.c index c640a807a..55ac5dc22 100644 --- a/postfix/src/global/mail_dict.c +++ b/postfix/src/global/mail_dict.c @@ -52,6 +52,7 @@ #include #include #include +#include #include #include #include @@ -71,6 +72,9 @@ static const DICT_OPEN_INFO dict_open_info[] = { #ifdef HAS_SQLITE DICT_TYPE_SQLITE, dict_sqlite_open, 0, #endif +#ifdef HAS_MONGODB + DICT_TYPE_MONGODB, dict_mongodb_open, 0, +#endif #endif /* !USE_DYNAMIC_MAPS */ DICT_TYPE_MEMCACHE, dict_memcache_open, 0, 0, diff --git a/postfix/src/global/mail_version.h b/postfix/src/global/mail_version.h index 98c9ccb32..34b806ed0 100644 --- a/postfix/src/global/mail_version.h +++ b/postfix/src/global/mail_version.h @@ -20,7 +20,7 @@ * Patches change both the patchlevel and the release date. Snapshots have no * patchlevel; they change the release date only. */ -#define MAIL_RELEASE_DATE "20240206" +#define MAIL_RELEASE_DATE "20240208" #define MAIL_VERSION_NUMBER "3.9" #ifdef SNAPSHOT diff --git a/postfix/src/postconf/Makefile.in b/postfix/src/postconf/Makefile.in index efae3652a..0aa29e888 100644 --- a/postfix/src/postconf/Makefile.in +++ b/postfix/src/postconf/Makefile.in @@ -17,7 +17,7 @@ MAKES = bool_table.h bool_vars.h int_table.h int_vars.h str_table.h \ nint_table.h nint_vars.h nbool_table.h nbool_vars.h long_table.h \ long_vars.h str_fn_table.h str_fn_vars.h DB_MAKES= pcf_ldap_suffixes.h pcf_memcache_suffixes.h pcf_mysql_suffixes.h \ - pcf_pgsql_suffixes.h pcf_sqlite_suffixes.h + pcf_pgsql_suffixes.h pcf_sqlite_suffixes.h pcf_mongodb_suffixes.h TEST_TMP= main.cf master.cf test*.tmp DUMMIES = makes_dummy # for "make -j" PROG = postconf @@ -79,6 +79,9 @@ pcf_ldap_suffixes.h: ../global/dict_ldap.c pcf_memcache_suffixes.h: ../global/dict_memcache.c sh extract_cfg.sh -d ../global/dict_memcache.c > $@ +pcf_mongodb_suffixes.h: ../global/dict_mongodb.c + sh extract_cfg.sh -d ../global/dict_mongodb.c > $@ + pcf_mysql_suffixes.h: ../global/dict_mysql.c sh extract_cfg.sh -d -s ../global/dict_mysql.c > $@ @@ -1149,6 +1152,7 @@ postconf_dbms.o: ../../include/dict_pgsql.h postconf_dbms.o: ../../include/dict_proxy.h postconf_dbms.o: ../../include/dict_regexp.h postconf_dbms.o: ../../include/dict_sqlite.h +postconf_dbms.o: ../../include/dict_mongodb.h postconf_dbms.o: ../../include/htable.h postconf_dbms.o: ../../include/mac_expand.h postconf_dbms.o: ../../include/mac_parse.h @@ -1170,6 +1174,7 @@ postconf_dbms.o: pcf_memcache_suffixes.h postconf_dbms.o: pcf_mysql_suffixes.h postconf_dbms.o: pcf_pgsql_suffixes.h postconf_dbms.o: pcf_sqlite_suffixes.h +postconf_dbms.o: pcf_mongodb_suffixes.h postconf_dbms.o: postconf.h postconf_dbms.o: postconf_dbms.c postconf_edit.o: ../../include/argv.h diff --git a/postfix/src/postfix/postfix.c b/postfix/src/postfix/postfix.c index 4f9f87d97..93bcecca5 100644 --- a/postfix/src/postfix/postfix.c +++ b/postfix/src/postfix/postfix.c @@ -316,6 +316,7 @@ /* ldap_table(5), Postfix LDAP client /* lmdb_table(5), Postfix LMDB database driver /* memcache_table(5), Postfix memcache client +/* mongodb_table(5), Postfix MongoDB client /* mysql_table(5), Postfix MYSQL client /* nisplus_table(5), Postfix NIS+ client /* pcre_table(5), Associate PCRE pattern with value diff --git a/postfix/src/postqueue/showq_json.c b/postfix/src/postqueue/showq_json.c index db7940462..a2820dda9 100644 --- a/postfix/src/postqueue/showq_json.c +++ b/postfix/src/postqueue/showq_json.c @@ -58,69 +58,6 @@ #define STR(x) vstring_str(x) #define LEN(x) VSTRING_LEN(x) -/* json_quote - quote JSON string */ - -static char *json_quote(VSTRING *result, const char *text) -{ - unsigned char *cp; - int ch; - - /* - * We use short escape sequences for common control characters. Note that - * RFC 4627 allows "/" (0x2F) to be sent without quoting. Differences - * with RFC 4627: we send DEL (0x7f) as \u007F; the result remains RFC - * 4627 complaint. - */ - VSTRING_RESET(result); - for (cp = (unsigned char *) text; (ch = *cp) != 0; cp++) { - if (UNEXPECTED(ISCNTRL(ch))) { - switch (ch) { - case '\b': - VSTRING_ADDCH(result, '\\'); - VSTRING_ADDCH(result, 'b'); - break; - case '\f': - VSTRING_ADDCH(result, '\\'); - VSTRING_ADDCH(result, 'f'); - break; - case '\n': - VSTRING_ADDCH(result, '\\'); - VSTRING_ADDCH(result, 'n'); - break; - case '\r': - VSTRING_ADDCH(result, '\\'); - VSTRING_ADDCH(result, 'r'); - break; - case '\t': - VSTRING_ADDCH(result, '\\'); - VSTRING_ADDCH(result, 't'); - break; - default: - vstring_sprintf_append(result, "\\u%04X", ch); - break; - } - } else { - switch (ch) { - case '\\': - case '"': - VSTRING_ADDCH(result, '\\'); - /* FALLTHROUGH */ - default: - VSTRING_ADDCH(result, ch); - break; - } - } - } - VSTRING_TERMINATE(result); - - /* - * Force the result to be UTF-8 (with SMTPUTF8 enabled) or ASCII (with - * SMTPUTF8 disabled). - */ - printable(STR(result), '?'); - return (STR(result)); -} - /* json_message - report status for one message */ static void format_json(VSTREAM *showq_stream) @@ -147,6 +84,12 @@ static void format_json(VSTREAM *showq_stream) quote_buf = vstring_alloc(100); } + /* + * Force JSON values to UTF-8 (with SMTPUTF8 enabled) or ASCII (with + * SMTPUTF8 disabled). + */ +#define QUOTE_JSON(res, src) printable(quote_for_json((res), (src), -1), '?') + /* * Read the message properties and sender address. */ @@ -162,14 +105,14 @@ static void format_json(VSTREAM *showq_stream) msg_fatal_status(EX_SOFTWARE, "malformed showq server response"); vstream_printf("{"); vstream_printf("\"queue_name\": \"%s\", ", - json_quote(quote_buf, STR(queue_name))); + QUOTE_JSON(quote_buf, STR(queue_name))); vstream_printf("\"queue_id\": \"%s\", ", - json_quote(quote_buf, STR(queue_id))); + QUOTE_JSON(quote_buf, STR(queue_id))); vstream_printf("\"arrival_time\": %ld, ", arrival_time); vstream_printf("\"message_size\": %ld, ", message_size); vstream_printf("\"forced_expire\": %s, ", forced_expire ? "true" : "false"); vstream_printf("\"sender\": \"%s\", ", - json_quote(quote_buf, STR(addr))); + QUOTE_JSON(quote_buf, STR(addr))); /* * Read zero or more (recipient, reason) pair(s) until attr_scan_more() @@ -188,10 +131,10 @@ static void format_json(VSTREAM *showq_stream) ATTR_TYPE_END) != 2) msg_fatal_status(EX_SOFTWARE, "malformed showq server response"); vstream_printf("\"address\": \"%s\"", - json_quote(quote_buf, STR(addr))); + QUOTE_JSON(quote_buf, STR(addr))); if (LEN(why) > 0) vstream_printf(", \"delay_reason\": \"%s\"", - json_quote(quote_buf, STR(why))); + QUOTE_JSON(quote_buf, STR(why))); vstream_printf("}"); } vstream_printf("]"); diff --git a/postfix/src/util/Makefile.in b/postfix/src/util/Makefile.in index 5c31d5e13..01211fba0 100644 --- a/postfix/src/util/Makefile.in +++ b/postfix/src/util/Makefile.in @@ -45,7 +45,7 @@ SRCS = alldig.c allprint.c argv.c argv_split.c attr_clnt.c attr_print0.c \ byte_mask.c known_tcp_ports.c argv_split_at.c dict_stream.c \ sane_strtol.c hash_fnv.c ldseed.c mkmap_cdb.c mkmap_db.c mkmap_dbm.c \ mkmap_fail.c mkmap_lmdb.c mkmap_open.c mkmap_sdbm.c inet_prefix_top.c \ - inet_addr_sizes.c + inet_addr_sizes.c quote_for_json.c OBJS = alldig.o allprint.o argv.o argv_split.o attr_clnt.o attr_print0.o \ attr_print64.o attr_print_plain.o attr_scan0.o attr_scan64.o \ attr_scan_plain.o auto_clnt.o base64_code.o basename.o binhash.o \ @@ -91,7 +91,8 @@ OBJS = alldig.o allprint.o argv.o argv_split.o attr_clnt.o attr_print0.o \ msg_logger.o logwriter.o unix_dgram_connect.o unix_dgram_listen.o \ byte_mask.o known_tcp_ports.o argv_split_at.o dict_stream.o \ sane_strtol.o hash_fnv.o ldseed.o mkmap_db.o mkmap_dbm.o \ - mkmap_fail.o mkmap_open.o inet_prefix_top.o inet_addr_sizes.o + mkmap_fail.o mkmap_open.o inet_prefix_top.o inet_addr_sizes.o \ + quote_for_json.o # MAP_OBJ is for maps that may be dynamically loaded with dynamicmaps.cf. # When hard-linking these, makedefs sets NON_PLUGIN_MAP_OBJ=$(MAP_OBJ), # otherwise it sets the PLUGIN_* macros. @@ -145,7 +146,7 @@ TESTPROG= dict_open dup2_pass_on_exec events exec_command fifo_open \ vstream timecmp dict_cache midna_domain casefold strcasecmp_utf8 \ vbuf_print split_qnameval vstream msg_logger byte_mask \ known_tcp_ports dict_stream find_inet binhash hash_fnv argv \ - clean_env inet_prefix_top printable readlline + clean_env inet_prefix_top printable readlline quote_for_json PLUGIN_MAP_SO = $(LIB_PREFIX)pcre$(LIB_SUFFIX) $(LIB_PREFIX)lmdb$(LIB_SUFFIX) \ $(LIB_PREFIX)cdb$(LIB_SUFFIX) $(LIB_PREFIX)sdbm$(LIB_SUFFIX) HTABLE_FIX = NORANDOMIZE=1 @@ -619,6 +620,11 @@ inet_prefix_top: $(LIB) $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(SYSLIBS) mv junk $@.o +quote_for_json: $(LIB) + mv $@.o junk + $(CC) $(CFLAGS) -DTEST -o $@ $@.c $(LIB) $(SYSLIBS) + mv junk $@.o + tests: all valid_hostname_test mac_expand_test dict_test unescape_test \ hex_quote_test ctable_test inet_addr_list_test base64_code_test \ attr_scan64_test attr_scan0_test host_port_test dict_tests \ @@ -629,7 +635,7 @@ tests: all valid_hostname_test mac_expand_test dict_test unescape_test \ miss_endif_regexp_test split_qnameval_test vstring_test \ vstream_test byte_mask_tests mystrtok_test known_tcp_ports_test \ binhash_test argv_test inet_prefix_top_test printable_test \ - valid_utf8_string_test readlline_test + valid_utf8_string_test readlline_test quote_for_json_test dict_tests: all dict_test \ dict_pcre_tests dict_cidr_test dict_thash_test dict_static_test \ @@ -1103,6 +1109,9 @@ argv_test: argv inet_prefix_top_test: inet_prefix_top $(SHLIB_ENV) ${VALGRIND} ./inet_prefix_top +quote_for_json_test: quote_for_json + $(SHLIB_ENV) ${VALGRIND} ./quote_for_json + depend: $(MAKES) (sed '1,/^# do not edit/!d' Makefile.in; \ set -e; for i in [a-z][a-z0-9]*.c; do \ @@ -2555,6 +2564,10 @@ printable.o: stringops.h printable.o: sys_defs.h printable.o: vbuf.h printable.o: vstring.h +quote_for_json.o: quote_for_json.c +quote_for_json.o: stringops.h +quote_for_json.o: sys_defs.h +quote_for_json.o: vstring.h rand_sleep.o: iostuff.h rand_sleep.o: msg.h rand_sleep.o: myrand.h diff --git a/postfix/src/util/quote_for_json.c b/postfix/src/util/quote_for_json.c new file mode 100644 index 000000000..f54af3fcc --- /dev/null +++ b/postfix/src/util/quote_for_json.c @@ -0,0 +1,218 @@ +/*++ +/* NAME +/* quote_for_json 3 +/* SUMMARY +/* quote UTF-8 string value for JSON +/* SYNOPSIS +/* #include +/* +/* char *quote_for_json( +/* VSTRING *result, +/* const char *in, +/* ssize_t len) +/* +/* char *quote_for_json_append( +/* VSTRING *result, +/* const char *in, +/* ssize_t len) +/* DESCRIPTION +/* quote_for_json() takes well-formed UTF-8 encoded text, +/* quotes that text compliant with RFC 4627, and returns a +/* pointer to the resulting text. The input may contain null +/* bytes, but the output will not. +/* +/* quote_for_json() produces short (two-letter) escape sequences +/* for common control characters, double quote and backslash. +/* It will not quote "/" (0x2F), and will quote DEL (0x7f) as +/* \u007F to make it printable. The input byte sequence "\uXXXX" +/* is quoted like any other text (the "\" is escaped as "\\"). +/* +/* quote_for_json() does not perform UTF-8 validation. The caller +/* should use valid_utf8_string() or printable() as appropriate. +/* +/* quote_for_json_append() appends the output to the result buffer. +/* +/* Arguments: +/* .IP result +/* Storage for the result, resized automatically. +/* .IP in +/* Pointer to the input byte sequence. +/* .IP len +/* The length of the input byte sequence, or a negative number +/* when the byte sequence is null-terminated. +/* DIAGNOSTICS +/* Fatal error: memory allocation error. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/* +/* Wietse Venema +/* porcupine.org +/*--*/ + + /* + * System library. + */ +#include +#include +#include + + /* + * Utility library. + */ +#include +#include + +#define STR(x) vstring_str(x) + +/* quote_for_json_append - quote JSON string, append result */ + +char *quote_for_json_append(VSTRING *result, const char *text, ssize_t len) +{ + const char *cp; + int ch; + + if (len < 0) + len = strlen(text); + + for (cp = text; len > 0; len--, cp++) { + ch = *(const unsigned char *) cp; + if (UNEXPECTED(ISCNTRL(ch))) { + switch (ch) { + case '\b': + VSTRING_ADDCH(result, '\\'); + VSTRING_ADDCH(result, 'b'); + break; + case '\f': + VSTRING_ADDCH(result, '\\'); + VSTRING_ADDCH(result, 'f'); + break; + case '\n': + VSTRING_ADDCH(result, '\\'); + VSTRING_ADDCH(result, 'n'); + break; + case '\r': + VSTRING_ADDCH(result, '\\'); + VSTRING_ADDCH(result, 'r'); + break; + case '\t': + VSTRING_ADDCH(result, '\\'); + VSTRING_ADDCH(result, 't'); + break; + default: + /* All other controls including DEL and NUL. */ + vstring_sprintf_append(result, "\\u%04X", ch); + break; + } + } else { + switch (ch) { + case '\\': + case '"': + VSTRING_ADDCH(result, '\\'); + /* FALLTHROUGH */ + default: + /* Includes malformed UTF-8. */ + VSTRING_ADDCH(result, ch); + break; + } + } + } + VSTRING_TERMINATE(result); + return (STR(result)); +} + +/* quote_for_json - quote JSON string */ + +char *quote_for_json(VSTRING *result, const char *text, ssize_t len) +{ + VSTRING_RESET(result); + return (quote_for_json_append(result, text, len)); +} + +#ifdef TEST + + /* + * System library. + */ +#include + + /* + * Utility library. + */ +#include +#include + +typedef struct TEST_CASE { + const char *label; /* identifies test case */ + char *(*fn) (VSTRING *, const char *, ssize_t); + const char *input; /* input string */ + ssize_t input_len; /* -1 or input length */ + const char *exp_res; /* expected result */ +} TEST_CASE; + +#define PASS (0) +#define FAIL (1) + + /* + * The test cases. + */ +static const TEST_CASE test_cases[] = { + {"ordinary ASCII text", quote_for_json, + " abcABC012.,[]{}/", -1, " abcABC012.,[]{}/", + }, + {"quote_for_json_append", quote_for_json_append, + "foo", -1, " abcABC012.,[]{}/foo", + }, + {"common control characters", quote_for_json, + "\b\f\r\n\t", -1, "\\b\\f\\r\\n\\t", + }, + {"uncommon control characters and DEL", quote_for_json, + "\0\01\037\040\176\177", 6, "\\u0000\\u0001\\u001F ~\\u007F", + }, + {"malformed UTF-8", quote_for_json, + "\\*\\uasd\\u007F\x80", -1, "\\\\*\\\\uasd\\\\u007F\x80", + }, + 0, +}; + +int main(int argc, char **argv) +{ + const TEST_CASE *tp; + int pass = 0; + int fail = 0; + VSTRING *res_buf = vstring_alloc(100); + + msg_vstream_init(sane_basename((VSTRING *) 0, argv[0]), VSTREAM_ERR); + + for (tp = test_cases; tp->label != 0; tp++) { + int test_fail = 0; + char *res; + + msg_info("RUN %s", tp->label); + res = tp->fn(res_buf, tp->input, tp->input_len); + if (strcmp(res, tp->exp_res) != 0) { + msg_warn("test case '%s': got '%s', want '%s'", + tp->label, res, tp->exp_res); + test_fail = 1; + } + if (test_fail) { + fail++; + msg_info("FAIL %s", tp->label); + test_fail = 1; + } else { + msg_info("PASS %s", tp->label); + pass++; + } + } + msg_info("PASS=%d FAIL=%d", pass, fail); + vstring_free(res_buf); + exit(fail != 0); +} + +#endif diff --git a/postfix/src/util/stringops.h b/postfix/src/util/stringops.h index ab38debbe..db56f237a 100644 --- a/postfix/src/util/stringops.h +++ b/postfix/src/util/stringops.h @@ -65,6 +65,8 @@ extern size_t balpar(const char *, const char *); extern char *WARN_UNUSED_RESULT extpar(char **, const char *, int); extern int strcasecmp_utf8x(int, const char *, const char *); extern int strncasecmp_utf8x(int, const char *, const char *, ssize_t); +extern char *quote_for_json(VSTRING *, const char *, ssize_t); +extern char *quote_for_json_append(VSTRING *, const char *, ssize_t); #define EXTPAR_FLAG_NONE (0) #define EXTPAR_FLAG_STRIP (1<<0) /* "{ text }" -> "text" */